zoukankan      html  css  js  c++  java
  • 打通es及lucene应用,lucene应用es Query,queryString Query获取及标准化

    打通es及lucene应用,lucene应用es Query,queryString Query获取及标准化

    调研思路

    • 基本示例

    看到这篇文章的,应该都有一定的es/lucene/大数据应用经验,很多概念也不好作更细的说明

    lucene索引的建立和检索,可以先看官方和第三方应用两个基本示例,预先有些了解

    官方

    https://lucene.apache.org/core/7_7_3/demo/src-html/org/apache/lucene/demo/IndexFiles.html

    https://lucene.apache.org/core/7_7_3/demo/src-html/org/apache/lucene/demo/SearchFiles.html

    第三方

    https://github.com/openimaj/openimaj/blob/master/text/nlp/src/main/java/org/openimaj/text/nlp/namedentity/QuickIndexer.java

    https://github.com/openimaj/openimaj/blob/master/text/nlp/src/main/java/org/openimaj/text/nlp/namedentity/QuickSearcher.java

    • lucene和es的兼容性问题

    索引的建立比较简单

    为简化问题,我们只考虑单分词字段(实际会用到全部的分词字段),实际我最初的目标也只是做针对分词字段,因为非分词字段,不论在hive/hbase/spark sql还是代码里实现逻辑过滤都不难,因此并没有考虑如何应用es内的其他term/range类查询

    但即使是对单分词field,es 对分词字段的query_string 和lucene 也并不完全兼容,即通过es search query_string 语法的查询字符串,直接应用在lucene上,命中结果并不一致

    脱离es环境,在大数据分布式场景下,直接应用lucene,并保持和es分词一致

    分别在两个方向进行调研

    • 1 从lucene向上,研究lucene的语法规则,把es的query string 转化为标准的lucene

    • 从es往下,es基于lucene作检索,必然有语法格式化相关的代码,因此如何定位和提取到es真正应用lucene的query对应,提取该对象应用在lucene上

    目前两个方向都搞定了

    方向一,测试结果ok,挑先出的测试用例都验证证确,保留态度

    方向二,测试结果ok,完美解决

    再强调,以下只是对单分词字段的应用,包含其他term,range等查询的条件并不涉及


    提取query-string Query对象调研过程

    代码量很小,实际工作量都在查看/逆向/拼接/提取 es代码上,其实工作状态和很早期做各种web/app爬虫逆向破解类似,但比爬虫这些简单多了,至少源码无混淆,ide跳转方遍,工程化特征明显(比如测试用例),结合es的使用经验,很方遍理解和定位特征

    这是两年前的代码了,当时es大版本为6.8 因此以下内容都针对es 6.8版本,7以上版本,对本文来说,基本没有影响,有时间再看看7的方案

    整体上7的变化主要是新功能支持(大部分还是xpack) 对历史的至少index search 部分影响不大

    另外,殊途同归,需求/目标不一致,逆向路径不同,个人经验不同,获取的信息不同,结果可能也不一致,不排除有更好的办法,这里只是抛砖引玉

    QueryString Query对象获取

    因为目标是对分词文本的检索,因此把目标锁定在QueryString

    首先查看代码结构,并通过关键词QueryStringQuery,定位到两个关键类

    QueryStringQueryBuilder 和 QueryStringQueryParser

    QueryStringQueryBuilder 会调用 org.elasticsearch.index.search.QueryStringQueryParser

    Screen Shot 2021-02-26 at 10.39.31 AM

    目标锁定至QueryStringQueryParser,QueryStringQueryParser该类下有大量方法反回query对象,重点关注的是parse方法,熟悉lucene的知道,这个是lucene的同名方法

    es QueryStringQueryParser内的parse方法

    Screen Shot 2021-02-26 at 10.41.59 AM

    lucene下QueryParserBase的同名基础parse方法,可以看到,包属于lucene

    Screen Shot 2021-02-26 at 1.32.43 PM

    实际es QueryStringQueryParser是lucene QueryParserBase的子类,继承关系如下

    public class QueryStringQueryParser extends XQueryParser {
                                   public class XQueryParser extends QueryParser {
                                                        public class QueryParser extends QueryParserBase implements QueryParserConstants {
    
    
        @Override
        public Query parse(String query) throws ParseException {
            if (query.trim().isEmpty()) {
                return Queries.newMatchNoDocsQuery("Matching no documents because no terms present");
            }
            return super.parse(query);
        }
    

    其实我的目标是parse方法构造出的Query对象,es的查询语句query_string部分,通过QueryStringQueryParser 执行parse,返回的Query对象,拿到这个Query对象最好能直接应到到lucene的查询上,如果无法应用,则若能转为标准的lucene语法,后续可应到到Lucene上

    实际到这里目的已经明确了,至少能看到希望了

    实始化QueryStringQueryParser,再调用parse(String query)方法即可

    但比较可惜的是QueryStringQueryParser的几个构造方法需要额外的几项参数,首当其冲的就是QueryShardContext 这个对象

    先看看 QueryShardContext 这个类

    Screen Shot 2021-02-26 at 10.50.10 AM

    看看这构造函数,除了一个重载的复制构造方法,真正的构造方法需要大量参数,也基本都是es集群的配置信息,这是正式启动es服务时,es服务应用的类

    public class QueryShardContext extends QueryRewriteContext {
    
        private final ScriptService scriptService;
        private final IndexSettings indexSettings;
        private final MapperService mapperService;
        private final SimilarityService similarityService;
        private final BitsetFilterCache bitsetFilterCache;
        private final Function<IndexReaderContext, IndexSearcher> searcherFactory;
        private final BiFunction<MappedFieldType, String, IndexFieldData<?>> indexFieldDataService;
        private final int shardId;
        private final IndexReader reader;
        private final String clusterAlias;
        private String[] types = Strings.EMPTY_ARRAY;
        private boolean cacheable = true;
        private final SetOnce<Boolean> frozen = new SetOnce<>();
        private final Index fullyQualifiedIndex;
    
        public void setTypes(String... types) {
            this.types = types;
        }
    
        public String[] getTypes() {
            return types;
        }
    
        private final Map<String, Query> namedQueries = new HashMap<>();
        private boolean allowUnmappedFields;
        private boolean mapUnmappedFieldAsString;
        private NestedScope nestedScope;
        private boolean isFilter;
    
        public QueryShardContext(int shardId, IndexSettings indexSettings, BitsetFilterCache bitsetFilterCache,
                                 Function<IndexReaderContext, IndexSearcher> searcherFactory,
                                 BiFunction<MappedFieldType, String, IndexFieldData<?>> indexFieldDataLookup, MapperService mapperService,
                                 SimilarityService similarityService, ScriptService scriptService, NamedXContentRegistry xContentRegistry,
                                 NamedWriteableRegistry namedWriteableRegistry, Client client, IndexReader reader, LongSupplier nowInMillis,
                                 String clusterAlias) {
            super(xContentRegistry, namedWriteableRegistry,client, nowInMillis);
            this.shardId = shardId;
            this.similarityService = similarityService;
            this.mapperService = mapperService;
            this.bitsetFilterCache = bitsetFilterCache;
            this.searcherFactory = searcherFactory;
            this.indexFieldDataService = indexFieldDataLookup;
            this.allowUnmappedFields = indexSettings.isDefaultAllowUnmappedFields();
            this.nestedScope = new NestedScope();
            this.scriptService = scriptService;
            this.indexSettings = indexSettings;
            this.reader = reader;
            this.clusterAlias = clusterAlias;
            this.fullyQualifiedIndex = new Index(RemoteClusterAware.buildRemoteIndexName(clusterAlias, indexSettings.getIndex().getName()),
                indexSettings.getIndex().getUUID());
        }
    
        public QueryShardContext(QueryShardContext source) {
            this(source.shardId, source.indexSettings, source.bitsetFilterCache, source.searcherFactory,
                source.indexFieldDataService, source.mapperService, source.similarityService, source.scriptService,
                source.getXContentRegistry(), source.getWriteableRegistry(), source.client, source.reader,
                source.nowInMillis, source.clusterAlias);
            this.types = source.getTypes();
        }
     
    }  
    

    构造方法较为复杂,我们找找其他QueryStringQueryParser实例构造的路径,首先想到的是找测试用例

    测试用例会有脱离es集群的独立调用,这里应该有构造的所有依赖,我们看看有没有QueryStringQueryParser相关的项

    果然找到了,基他类似的Test提前留意下

    Screen Shot 2021-02-26 at 10.18.30 AM

    Screen Shot 2021-02-26 at 11.01.18 AM

        public void testToQueryWildcardQuery() throws Exception {
            assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
            for (Operator op : Operator.values()) {
                BooleanClause.Occur defaultOp = op.toBooleanClauseOccur();
                QueryStringQueryParser queryParser = new QueryStringQueryParser(createShardContext(), STRING_FIELD_NAME);
                queryParser.setAnalyzeWildcard(true);
                queryParser.setMultiTermRewriteMethod(MultiTermQuery.CONSTANT_SCORE_REWRITE);
                queryParser.setDefaultOperator(op.toQueryParserOperator());
                Query query = queryParser.parse("first foo-bar-foobar* last");
                Query expectedQuery =
                    new BooleanQuery.Builder()
                        .add(new BooleanClause(new TermQuery(new Term(STRING_FIELD_NAME, "first")), defaultOp))
                        .add(new BooleanQuery.Builder()
                            .add(new BooleanClause(new TermQuery(new Term(STRING_FIELD_NAME, "foo")), defaultOp))
                            .add(new BooleanClause(new TermQuery(new Term(STRING_FIELD_NAME, "bar")), defaultOp))
                            .add(new BooleanClause(new PrefixQuery(new Term(STRING_FIELD_NAME, "foobar")), defaultOp))
                            .build(), defaultOp)
                        .add(new BooleanClause(new TermQuery(new Term(STRING_FIELD_NAME, "last")), defaultOp))
                        .build();
                assertThat(query, Matchers.equalTo(expectedQuery));
            }
        }
    

    目标代码 QueryStringQueryParser queryParser = new QueryStringQueryParser(createShardContext(), STRING_FIELD_NAME);

    通过createShardContext 构造出了QueryShardContext类实例

    Screen Shot 2021-02-26 at 11.13.14 AM

    到这里,理论上这条路就通了

    • 1 createShardContext 构造出了QueryShardContext类实例qsc

    • 2 通过qsc,构造出QueryStringQueryParser类实例qsqp

    • 3 调用qsqp.parse 得到Query对象q

    • 4 在lucene 上应用q

    分词处理

    还有另一重要问题没有解决,对分词字段来说,需要指定该分词应用的分词算法

    对正常的es集群服务,通过安装插件,建index,为index配置mapping,在mapping内指定field上应用的分词analyzer,也会指定es query filed的analyzer,查询时,es通过search和mapping内的信息,确定出使用哪个分词analyzer,再应用该analyzer执行计算

    但目前调研的方法,这种路径并不通,因为并没有一个完整的es环境,测试用例初始化的QueryShardContext,缺失很多信息

    这里的调研方向有两条

    • 1 做加法

    补全QueryShardContext的信息,尽量使QueryShardContext和真实的es环境一致,加载分词插件,注册mapping等,这种方法凭经验预估,难度会高些,但还是有些蛛丝马迹的

    比如QueryShardContext的构造函数里,会有indexSettings,mapperService,应该是indexSettings对应集群配置,mapperService对应es的mapping映射管理,追进代码里看也基本确定。

    public QueryShardContext(int shardId, IndexSettings indexSettings, BitsetFilterCache bitsetFilterCache,
                                 Function<IndexReaderContext, IndexSearcher> searcherFactory,
                                 BiFunction<MappedFieldType, String, IndexFieldData<?>> indexFieldDataLookup, MapperService mapperService,
                                 SimilarityService similarityService, ScriptService scriptService, NamedXContentRegistry xContentRegistry,
                                 NamedWriteableRegistry namedWriteableRegistry, Client client, IndexReader reader, LongSupplier nowInMillis,
                                 String clusterAlias) {
            super(xContentRegistry, namedWriteableRegistry,client, nowInMillis);
            this.shardId = shardId;
            this.similarityService = similarityService;
            this.mapperService = mapperService;
            this.bitsetFilterCache = bitsetFilterCache;
            this.searcherFactory = searcherFactory;
            this.indexFieldDataService = indexFieldDataLookup;
            this.allowUnmappedFields = indexSettings.isDefaultAllowUnmappedFields();
            this.nestedScope = new NestedScope();
            this.scriptService = scriptService;
            this.indexSettings = indexSettings;
            this.reader = reader;
            this.clusterAlias = clusterAlias;
            this.fullyQualifiedIndex = new Index(RemoteClusterAware.buildRemoteIndexName(clusterAlias, indexSettings.getIndex().getName()),
                indexSettings.getIndex().getUUID());
        }
        
    
    • 2 做减法

    强制指定分词

    因为我的目标只是中文分词,并且只会应用一种分词查询analyzer(不会说有两个字段,一个应用analyzer1,另一个应用analyzer2),因此可不可以跳过注册插件,注册mapping的部分,直接通过参数传入analyzer,并跳过从mapping获取analyzer的过程,直接使用传入的analyzer?

    预想是【做减法】比【做加法】简单,加法有额外的东西,也就有更多的依赖,减法反而是减少依赖的过程,先试试做减法的路子能不能走通,如果路子难走,再走做加法的路子。

    补充一点,以上只是经验之谈,实际也可能做加法更容易,做减法也容易引入一些其他问题

    例如,加法可能会很方便的找到缺失信息,简单就能补全。减法,如果少了一些信息,虽然可能最后走通了,但可能因为缺失信息,结果不达预期,还要掉头来补上,或放弃,重花时间走加法。

    加法也是更完美的办法


    最终比较顺利的,以做减法的方式,调通了

    调整主要是以下三项

    • 1 添加了一个QueryStringQueryParser 构造方法,添加参数Analyzer analyzer,这个参数会传入我真正期望的中文分词analyzer

    • 2 替换官方的 extractMultiFields方法,这个方法看起来是,通过具体的mapping 获取信息,但我这里压根就没有mapping,方法返回值需要Map<String, Float>,我先只返回个单字段的mapping即可,看看效果

    • 3 更改代码内的analyzer 为我传入的analyzer

    Analyzer oldAnalyzer = queryBuilder.analyzer

    public class QueryStringQueryParser extends XQueryParser {
        //因为个的场景只使用一种分词算法,所以使用了全局的静态变量,必要的话可以改为实例变量,结构不影响,对本文来说,只是保存传入analyzer
        public static Analyzer defaultAnalyzer;
        static {
            defaultAnalyzer = new StandardAnalyzer();
        }
        //传入analyzer
        public QueryStringQueryParser(QueryShardContext context, String defaultField, Analyzer analyzer) {
            this(context, defaultField, Collections.emptyMap(), false, analyzer);
            QueryStringQueryParser.defaultAnalyzer = analyzer;
            MatchQuery.defaultAnalyzer = analyzer;
        }
    
    //    这里官方原生的的extractMultiFields,可以大概看到是从resolveMappingField 解析
    //    private Map<String, Float> extractMultiFields(String field, boolean quoted) {
    //        if (field != null) {
    //            boolean allFields = Regex.isMatchAllPattern(field);
    //            if (allFields && this.field != null && this.field.equals(field)) {
    //                // "*" is the default field
    //                return fieldsAndWeights;
    //            }
    //            boolean multiFields = Regex.isSimpleMatchPattern(field);
    //            // Filters unsupported fields if a pattern is requested
    //            // Filters metadata fields if all fields are requested
    //            return resolveMappingField(context, field, 1.0f, !allFields, !multiFields, quoted ? quoteFieldSuffix : null);
    //        } else if (quoted && quoteFieldSuffix != null) {
    //            return resolveMappingFields(context, fieldsAndWeights, quoteFieldSuffix);
    //        } else {
    //            return fieldsAndWeights;
    //        }
    //    }
      
    //    个人重写的,因为只对单字段应用分词,直接指定一个field,这个filed的名称需要注意,如果后期使用正则替换的话
          private Map<String, Float> extractMultiFields(String field, boolean quoted) {
            Map<String, Float> fields = new HashMap<>();
            fields.put(field, 1.0f);
            //todo ...
            return fields;
        }
      
        @Override
        protected Query getFieldQuery(String field, String queryText, int slop) throws ParseException {
            if (field != null && EXISTS_FIELD.equals(field)) {
                return existsQuery(queryText);
            }
    
            Map<String, Float> fields = extractMultiFields(field, true);
            if (fields.isEmpty()) {
                return newUnmappedFieldQuery(field);
            }
    // 官方原生的使用的是queryBuilder.analyzer
    //        Analyzer oldAnalyzer = queryBuilder.analyzer;
    // 为减小复杂度,改为指定传入的
            Analyzer oldAnalyzer = defaultAnalyzer;
    //        int oldSlop = queryBuilder.phraseSlop;
            int oldSlop = 0;
            try {
                if (forceQuoteAnalyzer != null) {
                    queryBuilder.setAnalyzer(forceQuoteAnalyzer);
                } else if (forceAnalyzer != null) {
                    queryBuilder.setAnalyzer(forceAnalyzer);
                }
                queryBuilder.setPhraseSlop(slop);
                Query query = queryBuilder.parse(MultiMatchQueryBuilder.Type.PHRASE, fields, queryText, null);
                if (query == null) {
                    return null;
                }
                return applySlop(query, slop);
            } catch (IOException e) {
                throw new ParseException(e.getMessage());
            } finally {
                queryBuilder.setAnalyzer(oldAnalyzer);
                queryBuilder.setPhraseSlop(oldSlop);
            }
        }  
    

    这个调研做的比较早了,写这篇文章是后续的梳理,当时梳理,按思路走找到一个可行的点就试,也没有考虑优化,和是否有其他更好的方法

    我当时直接为快速验证,直接把QueryStringQueryParser.java内的所有Analyzer 实例都替换成defaultAnalyzer了

    现在回头一看,发现了forceAnalyzer,源代码也公开了写入

    public void setForceAnalyzer(Analyzer analyzer) {
        this.forceAnalyzer = analyzer;
    }
    
    //            Analyzer normalizer = forceAnalyzer == null ? queryBuilder.context.getSearchAnalyzer(currentFieldType) : forceAnalyzer;
                Analyzer normalizer = defaultAnalyzer;
    

    指定forceAnalyzer的话,应该可以不需要通过构造函数传入defaultAnalyzer了,有空做验证

    基于源码更改的操作,要尽量作到少改动,改动越少,风险越小,如果验证setForceAnalyzer可行,会作方案变更,指定analyzer的方式不同而已,不影响本文的思路

    接下来我们可以操作了,简而言就是

    //构造shard信息
    QueryShardContext qc = AbstractQueryTestCase.createShardContext(); 
    //构造QueryStringQueryParser类 因为是分词索引
    //以es的数据结构,对分词字段,要为分词字段指定分词插件,除了es自带的,中文场景下,会用ik/hanlp/ansj等分词器
    QueryParser queryStringQueryParser = new QueryStringQueryParser(qc, "text_field", analyzer);
    //执行语法解析
    Query query = queryStringQueryParser.parse(esQueryStringPharse);
    //实际这个query对象就可以拿来用了,直接应到到lucene上了
    

    大路通顺后,其实还有很多细节没有提到,实际AbstractQueryTestCase.createShardContext()这里也花了个人较多的时间

    部分代码,依然有大量的外部依赖要去除/重载/置为null/重写等,这些较为碎片就不提了,这些也都是风险点,因为精简掉的部分,可能会影响最终的结果

    好在还算顺利,操作完成后去掉依赖,验证结果依然保证了准确

    以上是提取query对象的部分


    es Query应用至Lucene

    query对象较为基础,因为query本身就是lucene类QueryParserBase的实例,上方提取出的是QueryParserBase的子类实现,本身就是应用lucene查询而已,可以直接由lucene调用

    项目相关代码就不放了,以官方示例说明,后期会开发并公开一些demo性质的,可能大数据相关的示例

    官方文档
    https://lucene.apache.org/core/8_8_1/index.html
    https://lucene.apache.org/core/7_7_3/index.html

    索引index

    https://lucene.apache.org/core/7_7_3/demo/src-html/org/apache/lucene/demo/IndexFiles.html

    lucene下分词字段的类型为TextField

    doc.add(new TextField("contents", new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))));

    /** Indexes a single document */
    static void indexDoc(IndexWriter writer, Path file, long lastModified) throws IOException {
      try (InputStream stream = Files.newInputStream(file)) {
        Document doc = new Document();
        Field pathField = new StringField("path", file.toString(), Field.Store.YES);
        doc.add(pathField);
        doc.add(new LongPoint("modified", lastModified));
        doc.add(new TextField("contents", new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))));
        if (writer.getConfig().getOpenMode() == OpenMode.CREATE) {
          System.out.println("adding " + file);
          writer.addDocument(doc);
        } else {
          System.out.println("updating " + file);
          writer.updateDocument(new Term("path", file.toString()), doc);
        }
      }
    }
    

    索引比较简单不做说明,主要是确定分词和存储方式,大数据应用,用在内存里较多,内存建议索引,直接应用查询,本来也是项目方案的目的,在其他场景下也可以考虑落地或其他文件服务。这个是后话了

    检索search

    https://lucene.apache.org/core/7_7_3/demo/src-html/org/apache/lucene/demo/SearchFiles.html

      public static void main(String[] args) throws Exception {
        ...    
        IndexReader reader = DirectoryReader.open(FSDirectory.open(Paths.get(index)));
        IndexSearcher searcher = new IndexSearcher(reader);
        Analyzer analyzer = new StandardAnalyzer();
    
        BufferedReader in = null;
        if (queries != null) {
          in = Files.newBufferedReader(Paths.get(queries), StandardCharsets.UTF_8);
        } else {
          in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
        }
        QueryParser parser = new QueryParser(field, analyzer);
        while (true) {
          String line = queryString != null ? queryString : in.readLine();
          ...
          Query query = parser.parse(line);
          System.out.println("Searching for: " + query.toString(field));
                
          if (repeat > 0) {                           // repeat & time as benchmark
            Date start = new Date();
            for (int i = 0; i < repeat; i++) {
              searcher.search(query, 100);
            }
            Date end = new Date();
            System.out.println("Time: "+(end.getTime()-start.getTime())+"ms");
          }
        }
        reader.close();
      }
    

    searcher.search(query, 100); 部分,这里的query对象,即可使用上方提取出的es生成的对象,当然前提是建索引时的file_name一致

    即建立时

    doc.add(new TextField("contents", new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))));

    查找时

    QueryParser queryStringQueryParser = new QueryStringQueryParser(qc, "contents", analyzer);

    实际contents会传入应用fields.put(field, 1.0f);

    private Map<String, Float> extractMultiFields(String field, boolean quoted) {
        Map<String, Float> fields = new HashMap<>();
        fields.put(field, 1.0f);
        //todo ...
        return fields;
    }
    

    fields.put(field, 1.0f); 我最早的方案只传入单个key,因此es生成的query也是针对这个key的如,但lucene自定义检索时可能是多key

    这个问题有三种方法解决

    • 1 这里实现多key
    private Map<String, Float> extractMultiFields(String field, boolean quoted) {
        Map<String, Float> fields = new HashMap<>();
        fields.put("field", 1.0f);
        fields.put("field", 1.0f);
        //todo ...
        return fields;
    }
    
    • 2 对每个key分别应用,生成对应的query对象,分别应用search 后并按需取交并 即should/must

    • 3 打印出es转换出的string,这里已经转为标准lucene语法了,以该string重新构建query,直接查询 (其实这也是我一开始的目的,但发现其他两种方法更好)

    提取出标准lucene方法如下

    Query对象,自带toString方法,如果 fields.put("`filed_name", 1.0f);只传入一项field,那输出的就是针对此单key的查询

    打印现来会发现 输出内容会带上filed_name:前缀

    例如

    QueryParser queryStringQueryParser = new QueryStringQueryParser(qc, "text_field", analyzer);

    输出内容会带上"text_field:"

    如果就是要用"text_field:"查询,那直接应用Query即可,如果查询字段不一致,需要重新构造,则需要提取不含"text_field:"的部分,也即替换掉"text_field:"

    正则替换即可,考虑性能问题的话,常用的es query_string查询可以预先通过该方法转为lucene 语法,预先保存映射关系

    lucenePharse = query.toString().replaceAll("text_field:", "");
    

    lucenePharse即是标准的lucene语法

    之后以lucenePharse为参数重新构造一个lucene的多字段Query,应用即可

    隐患

    有经验的已经看出问题了,如果"text_field:"是个单词,会因为这种替换丢失,这样导致查询内容和原生es内容不同,查询结果就可能不一致

    现在这个方案,最low的地方就是这里"text_field:"的替换,但实际分词的文本中可能会包含"text_field:"这个字符串,因此,最好选择绝不会应用的值

    实际业务应用和工作中,"text_field:"是个单词的概率几乎没有,担心影响的话,可以把text_field 换为MD5 ("text_field") = ebfdbe4a280b61b7142a54132d34b993 一类的值,更减少出问题的概率,缺点是正则性能损失一些

    这个隐患,这样几乎能作到完全避免了,但还是留意下,如果还觉得有问题,可以换其他方案,直接应用Query对象,这也是更推荐的办法

    最终代码如下,因为是以测试用例为入口,部分初始化工作在 c.beforeTest()中,因此看起来会有些奇怪

        public static String formatToLucenePhrase(String esQueryStringPharse, Analyzer analyzer) throws Exception {
            AbstractQueryTestCase c = new AbstractQueryTestCase();
            c.beforeTest();
            QueryShardContext qc = AbstractQueryTestCase.createShardContext();
            QueryParser queryStringQueryParser = new QueryStringQueryParser(qc, "text_content", analyzer);
            Query query = queryStringQueryParser.parse(esQueryStringPharse);
            String lucenePharse;
            lucenePharse = query.toString().replaceAll("text_context:", "");
            System.out.println("es query_string orgnial " + esQueryStringPharse);
            System.out.println("es query_string pharse " + query);
            System.out.println("es query_string term " + lucenePharse);
            return lucenePharse;
        }
    

    这只是提取query,提取query文本的部分,需要传入真正实际应用的analyzer才好看到效果,后续会给几个实际应用的demo

    提出标准lucene功能的额外应用

    我个人验证时主要担心query_string的实际的查询量和lucene的查询量不一致,lucene不能直接用query_string的话法,因为query_string有包装,es是能直接用lucene语法的,因此我验证时的一个方式就是es的query_string 和query_string转换提出lucene的语法,分别查询es,查看最终结果是否一致(另一个验证方法是,从大数hdfs/hbase/hive拉文本,lucene构建,lucene查询,再比较相同数据es索引/检索的数据量)

    结果是完全匹配的,不匹配的话,现在的方案就是失败的

    因为实际应用的query_string,并不像示例的那样简单,可能有几千,几万词,各种词距离,编辑距离,布尔,嵌套关系等,文本长度甚至超过100k,这样的query_string不可能全是每次独立拼接的,而是各种父子语句结合应用

    意外收获是因为产品业务为拼装组合的逻辑生成的query_string不一定是最优的,可能会有重复和失效的部分,但是经过解析和提取后 query_string实际在不损失内容的情况下,大大的优化了,更清晰,文本长度更小

    本来我保存es query_string和 其对应标准lucene语法是为了缓存,减少重复计算。

    意外发发现精简query_string的查询也挺有用,长度100k的query string 甚至可以减少到60k

    !还是要强调下,这种提取方式是有隐患,隐患虽然几乎可以完全避免,但需要留意这个问题

  • 相关阅读:
    System.ServiceModel.CommunicationException:The underlying connection was closed: The connection was closed unexpectedly
    研究jBPM和NetBPM
    研究:Microsoft Solution Framework
    iOS银联,支付宝,微信,ping++开发文档
    Xampp+Openfire+Spark的简单使用
    ObjectiveC与Swift混编
    网络抓包教程
    iOS版微信开发小结(微信支付,APP跳转微信公众号)
    七牛上传图片问题总结
    2012年之总结
  • 原文地址:https://www.cnblogs.com/zihunqingxin/p/14461627.html
Copyright © 2011-2022 走看看