热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

lucene源码分析---7

lucene源码分析—QueryParser的parse函数本章主要分析QueryParser类的parse函数,定义在其父类QueryParserBase中,QueryParserBas

lucene源码分析—QueryParser的parse函数

本章主要分析QueryParser类的parse函数,定义在其父类QueryParserBase中,
QueryParserBase::parse

  public Query parse(String query) throws ParseException {
ReInit(new FastCharStream(new StringReader(query)));
try {
Query res = TopLevelQuery(field);
return res!=null ? res : newBooleanQuery().build();
} catch (ParseException | TokenMgrError tme) {

} catch (BooleanQuery.TooManyClauses tmc) {

}
}

parse首先将需要搜索的字符串query封装成FastCharStream,FastCharStream实现了Java的CharStream接口,内部使用了一个缓存,并且可以方便读取并且改变读写指针。然后调用ReInit进行初始化,ReInit以及整个QueryParser都是由JavaCC根据org.apache.lucene.queryparse.classic.QueryParser.jj文件自动生成,设计到的JavaCC的知识可以从网上或者别的书上查找,本博文不会重点分析这块内容。
parse最重要的函数是TopLevelQuery,即返回顶层Query,TopLevelQuery会根据用来搜索的字符串query创建一个树形的Query结构,传入的参数field在QueryParserBase的构造函数中赋值,用来标识对哪个域进行搜索。
QueryParserBase::parse->QueryParser::TopLevelQuery

  final public Query TopLevelQuery(String field) throws ParseException {
Query q;
q = Query(field);
jj_consume_token(0);
{
if (true) return q;
}
throw new Error();
}

TopLevelQuery函数中最关键的是Query函数,由于QueryParser由JavaCC生成,这里只看QueryParser.jj文件。

QueryParser.jj::Query

Query Query(String field) :
{
List<BooleanClause> clauses = new ArrayList<BooleanClause>();
Query q, firstQuery=null;
int conj, mods;
}
{
mods=Modifiers() q=Clause(field)
{
addClause(clauses, CONJ_NONE, mods, q);
if (mods == MOD_NONE)
firstQuery=q;
}
(
conj=Conjunction() mods=Modifiers() q=Clause(field)
{ addClause(clauses, conj, mods, q); }
)*
{
if (clauses.size() == 1 && firstQuery != null)
return firstQuery;
else {
return getBooleanQuery(clauses);
}
}
}

Modifiers返回搜索字符串中的”+”或”-“,Conjunction返回连接字符串。Query首先通过Clause函数返回一个子查询,然后调用addClause函数添加该子查询,

QueryParserBase::addClause

  protected void addClause(List clauses, int conj, int mods, Query q) {
boolean required, prohibited;

...

if (required && !prohibited)
clauses.add(newBooleanClause(q, BooleanClause.Occur.MUST));
else if (!required && !prohibited)
clauses.add(newBooleanClause(q, BooleanClause.Occur.SHOULD));
else if (!required && prohibited)
clauses.add(newBooleanClause(q, BooleanClause.Occur.MUST_NOT));
else
throw new RuntimeException("Clause cannot be both required and prohibited");
}

addClause函数中省略的部分是根据参数连接符conj和mods计算required和prohibited的值,然后将Query封装成BooleanClause并添加到clauses列表中。

回到Query函数中,如果子查询clauses列表只有一个子查询,就直接返回,否则通过getBooleanQuery函数封装所有的子查询并最终返回一个BooleanClause。

下面来看Clause函数,即创建一个子查询,
QueryParser.jj::Clause

Query Clause(String field) : {
Query q;
Token fieldToken=null, boost=null;
}
{
[
LOOKAHEAD(2)
(
fieldToken=<TERM> <COLON> {field=discardEscapeChar(fieldToken.image);}
| <STAR> <COLON> {field="*";}
)
]

(
q=Term(field)
| <LPAREN> q=Query(field) <RPAREN> (<CARAT> boost=<NUMBER>)?

)
{ return handleBoost(q, boost); }
}

LOOKAHEAD(2)表示要看两个符号,如果是Field,则要重新调整搜索的域。Clause函数最重要的是Term函数,该函数返回最终的Query,当然Clause函数也可以嵌套调用Query函数生成子查询。

QueryParser.jj::Term

Query Term(String field) : {
Token term, boost=null, fuzzySlop=null, goop1, goop2;
boolean prefix = false;
boolean wildcard = false;
boolean fuzzy = false;
boolean regexp = false;
boolean startInc=false;
boolean endInc=false;
Query q;
}
{
(
(
term=
| term= { wildcard=true; }
| term= { prefix=true; }
| term= { wildcard=true; }
| term= { regexp=true; }
| term=
| term= { term.image = term.image.substring(0,1); }
)
[ fuzzySlop= { fuzzy=true; } ]
[ boost= [ fuzzySlop= { fuzzy=true; } ] ]
{
q = handleBareTokenQuery(field, term, fuzzySlop, prefix, wildcard, fuzzy, regexp);
}
| ( ( {startInc=true;} | )
( goop1=|goop1= )
[ ]
( goop2=|goop2= )
( {endInc=true;} | ))
[ boost= ]
{
boolean startOpen=false;
boolean endOpen=false;
if (goop1.kind == RANGE_QUOTED) {
goop1.image = goop1.image.substring(1, goop1.image.length()-1);
} else if ("*".equals(goop1.image)) {
startOpen=true;
}
if (goop2.kind == RANGE_QUOTED) {
goop2.image = goop2.image.substring(1, goop2.image.length()-1);
} else if ("*".equals(goop2.image)) {
endOpen=true;
}
q = getRangeQuery(field, startOpen ? null : discardEscapeChar(goop1.image), endOpen ? null : discardEscapeChar(goop2.image), startInc, endInc);
}
| term=
[ fuzzySlop= ]
[ boost= ]
{ q = handleQuotedTerm(field, term, fuzzySlop); }
)
{ return handleBoost(q, boost); }
}

如果一个查询不包括引号(QUOTED),边界符号(RANGE,例如小括号、中括号等),大部分情况下最终会通过handleBareTokenQuery函数生成一个Term,代表一个词,然后被封装成一个子查询Clause,最后被封装成一个Query,Clause和Query互相嵌套,即一个Query里可以包含多个Clause,一个Clause里又可以从一个Query开始,最终的叶子节点就是Term对应的Query。

QueryParserBase::handleBareTokenQuery

  Query handleBareTokenQuery(String qfield, Token term, Token fuzzySlop, boolean prefix, boolean wildcard, boolean fuzzy, boolean regexp) throws ParseException {
Query q;

String termImage=discardEscapeChar(term.image);
if (wildcard) {
q = getWildcardQuery(qfield, term.image);
} else if (prefix) {
q = getPrefixQuery(qfield,
discardEscapeChar(term.image.substring
(0, term.image.length()-1)));
} else if (regexp) {
q = getRegexpQuery(qfield, term.image.substring(1, term.image.length()-1));
} else if (fuzzy) {
q = handleBareFuzzy(qfield, fuzzySlop, termImage);
} else {
q = getFieldQuery(qfield, termImage, false);
}
return q;
}

举例来说,查询字符串AAA*代表prefix查询,此时参数prefix为真,A*A代表wildcard查询,此时参数wildcard为真,AA~代表fuzzy模糊查询,此时参数fuzzy为真。这里假设三个都不为真,就是一串平常的单词,最后会通过getFieldQuery生成一个Query,本文重点分析该函数。

QueryParserBase::handleBareTokenQuery->getFieldQuery

  protected Query getFieldQuery(String field, String queryText, boolean quoted) throws ParseException {
return newFieldQuery(getAnalyzer(), field, queryText, quoted);
}

protected Query newFieldQuery(Analyzer analyzer, String field, String queryText, boolean quoted) throws ParseException {
BooleanClause.Occur occur = operator == Operator.AND ? BooleanClause.Occur.MUST : BooleanClause.Occur.SHOULD;
return createFieldQuery(analyzer, occur, field, queryText, quoted || autoGeneratePhraseQueries, phraseSlop);
}

getAnalyzer返回QueryParserBase的init函数中设置的分词器,这里为了方便分析,假设为SimpleAnalyzer。quoted以及autoGeneratePhraseQueries表示是否创建PhraseQuery,phraseSlop为位置因子,只有PhraseQuery用得到,这里不管它。下面来看createFieldQuery函数。
QueryParserBase::handleBareTokenQuery->getFieldQuery->newFieldQuery->QueryBuilder::createFieldQuery

  protected final Query createFieldQuery(Analyzer analyzer, BooleanClause.Occur operator, String field, String queryText, boolean quoted, int phraseSlop) {

try (TokenStream source = analyzer.tokenStream(field, queryText);
CachingTokenFilter stream = new CachingTokenFilter(source)) {

TermToBytesRefAttribute termAtt = stream.getAttribute(TermToBytesRefAttribute.class);
PositionIncrementAttribute posIncAtt = stream.addAttribute(PositionIncrementAttribute.class);

int numTokens = 0;
int positiOnCount= 0;
boolean hasSynOnyms= false;

stream.reset();
while (stream.incrementToken()) {
numTokens++;
int positiOnIncrement= posIncAtt.getPositionIncrement();
if (positionIncrement != 0) {
positionCount += positionIncrement;
} else {
hasSynOnyms= true;
}
}

if (numTokens == 0) {
return null;
} else if (numTokens == 1) {
return analyzeTerm(field, stream);
} else if (quoted && positionCount > 1) {
...
} else {
if (positiOnCount== 1) {
return analyzeBoolean(field, stream);
} else {
return analyzeMultiBoolean(field, stream, operator);
}
}
} catch (IOException e) {

}
}

关于分词器的tokenStream以及incrementToken函数在《lucene源码分析—4》中分析过了。直接看最后的结果,假设numTokens==1,则分词器的输出结果只有一个词,则使用analyzeTerm创建最终的Query;
假设positiOnCount== 1,则表示结果中多个词出现在同一个位置,此时使用analyzeBoolean创建Query;剩下情况表示有多个词,至少两个词出现在不同位置,使用analyzeMultiBoolean创建Query。本文只分析analyzeMultiBoolean函数,
QueryParserBase::handleBareTokenQuery->getFieldQuery->newFieldQuery->QueryBuilder::createFieldQuery->analyzeMultiBoolean

  private Query analyzeMultiBoolean(String field, TokenStream stream, BooleanClause.Occur operator) throws IOException {
BooleanQuery.Builder q = newBooleanQuery();
List currentQuery = new ArrayList<>();

TermToBytesRefAttribute termAtt = stream.getAttribute(TermToBytesRefAttribute.class);
PositionIncrementAttribute posIncrAtt = stream.getAttribute(PositionIncrementAttribute.class);

stream.reset();
while (stream.incrementToken()) {
if (posIncrAtt.getPositionIncrement() != 0) {
add(q, currentQuery, operator);
currentQuery.clear();
}
currentQuery.add(new Term(field, termAtt.getBytesRef()));
}
add(q, currentQuery, operator);

return q.build();
}

private void add(BooleanQuery.Builder q, List current, BooleanClause.Occur operator) {
if (current.isEmpty()) {
return;
}
if (current.size() == 1) {
q.add(newTermQuery(current.get(0)), operator);
} else {
q.add(newSynonymQuery(current.toArray(new Term[current.size()])), operator);
}
}

public Builder add(Query query, Occur occur) {
clauses.add(new BooleanClause(query, occur));
return this;
}

分词器的输出结果保存在TermToBytesRefAttribute中,analyzeMultiBoolean函数将同一个起始位置不同的Term添加到列表currentQuery中,如果同一个位置只有一个Term,则将其封装成TermQuery,如果有多个Term,就封装成SynonymQuery,TermQuery和SynonymQuery最后被封装成BooleanClause,添加到BooleanQuery.Builder中的一个BooleanClause列表中。最后通过BooleanQuery.Builder的build函数根据内置的BooleanClause列表创建一个最终的BooleanClause。


推荐阅读
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • 本文介绍了C++中省略号类型和参数个数不确定函数参数的使用方法,并提供了一个范例。通过宏定义的方式,可以方便地处理不定参数的情况。文章中给出了具体的代码实现,并对代码进行了解释和说明。这对于需要处理不定参数的情况的程序员来说,是一个很有用的参考资料。 ... [详细]
  • 摘要: 在测试数据中,生成中文姓名是一个常见的需求。本文介绍了使用C#编写的随机生成中文姓名的方法,并分享了相关代码。作者欢迎读者提出意见和建议。 ... [详细]
  • 本文讨论了Kotlin中扩展函数的一些惯用用法以及其合理性。作者认为在某些情况下,定义扩展函数没有意义,但官方的编码约定支持这种方式。文章还介绍了在类之外定义扩展函数的具体用法,并讨论了避免使用扩展函数的边缘情况。作者提出了对于扩展函数的合理性的质疑,并给出了自己的反驳。最后,文章强调了在编写Kotlin代码时可以自由地使用扩展函数的重要性。 ... [详细]
  • 第四章高阶函数(参数传递、高阶函数、lambda表达式)(python进阶)的讲解和应用
    本文主要讲解了第四章高阶函数(参数传递、高阶函数、lambda表达式)的相关知识,包括函数参数传递机制和赋值机制、引用传递的概念和应用、默认参数的定义和使用等内容。同时介绍了高阶函数和lambda表达式的概念,并给出了一些实例代码进行演示。对于想要进一步提升python编程能力的读者来说,本文将是一个不错的学习资料。 ... [详细]
  • 本文介绍了在Python3中如何使用选择文件对话框的格式打开和保存图片的方法。通过使用tkinter库中的filedialog模块的asksaveasfilename和askopenfilename函数,可以方便地选择要打开或保存的图片文件,并进行相关操作。具体的代码示例和操作步骤也被提供。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • PHP图片截取方法及应用实例
    本文介绍了使用PHP动态切割JPEG图片的方法,并提供了应用实例,包括截取视频图、提取文章内容中的图片地址、裁切图片等问题。详细介绍了相关的PHP函数和参数的使用,以及图片切割的具体步骤。同时,还提供了一些注意事项和优化建议。通过本文的学习,读者可以掌握PHP图片截取的技巧,实现自己的需求。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 树莓派Linux基础(一):查看文件系统的命令行操作
    本文介绍了在树莓派上通过SSH服务使用命令行查看文件系统的操作,包括cd命令用于变更目录、pwd命令用于显示当前目录位置、ls命令用于显示文件和目录列表。详细讲解了这些命令的使用方法和注意事项。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 本文介绍了PE文件结构中的导出表的解析方法,包括获取区段头表、遍历查找所在的区段等步骤。通过该方法可以准确地解析PE文件中的导出表信息。 ... [详细]
author-avatar
leee
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有