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

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

https://github.com/cclient/elasticsearch-query-string-lucene-format

最终目标是在外部程序内构建lucene索引,并保证和es查询的兼容性(主要是全文检索部分),通过异构存储加载doc计算检索命中,减少es的压力

调研思路

  • 基本示例

看到这篇文章的,应该都有一定的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,应用即可

es-query -> string -> 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实际在不损失内容的情况下,大大的优化了,更清晰,文本长度更小,实现了dsl语句的压缩和优化

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

意外发现精简query_string的查询的用途,长度100k的query string 可以减少一半

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

posted @ 2021-03-01 07:39  cclient  阅读(1409)  评论(0编辑  收藏  举报