使用 Apache Lucene 和 Solr 4 实现下一代搜索和分析

[复制链接]
查看11 | 回复2 | 2007-1-24 12:56:49 | 显示全部楼层 |阅读模式
浏览该数据[size=1.166em]Solr Air 应用程序正常运行后,您就可以浏览该数据,查看 UI 以了解您可以询问的问题类型。在浏览器中,您应看到两个主要的接口点:地图和搜索框。对于地图,我首先从 D3 的优秀的 Airport 示例开始介绍(参见 参考资料)。我修改并扩展了该代码,以便直接从 Solr 加载所有机场信息,而不是从 D3 示例随带的示例 SCV 文件进行加载。我还对每个机场执行了一些初步的统计计算,您可以将鼠标悬停在一个特定机场上来查看该信息。[size=1.166em]我将使用搜索框展示一些可帮助您构建复杂的搜索和分析应用程序的重要功能。要理解该代码,请参阅 solr/collection1/conf/velocity/map.vm 文件。[size=1.166em]重要的关注区域包括:中心点分面统计功能分组Lucene 和 Solr 扩展的地理空间支持
[size=1.166em]每个区域都可以帮助您回答一些问题,比如抵达一个特定机场的航班的平均晚点时间,或者在两个机场之间飞行的一个飞机的最常见的晚点时间(根据航线进行确定,或者根据某个起飞机场与所有邻近机场之间的距离来确定)。该应用程序使用 Solr 的统计功能,再结合 Solr 存在已久的分面功能来绘制机场 “点” 的初始地图并生成基本信息,比如航班总数,平均、最短和最长晚点时间。(单单此功能就是一种查找坏数据或至少查找极端异常值的出色方法。)为了演示这些区域(并展示如何轻松地集成 Solr 与 D3),我实现了一些轻量型 JavaScript 代码,以执行以下操作:分析查询。(一个具有生产品质的应用程序可能会在服务器端执行大部分查询,甚至被用作一个 Solr 查询分析器插件。)创建各种 Solr 请求。显示结果。
[size=1.166em]结果类型包括:按 3 字母机场代码(比如 RDU 或 SFO)的查找。按路线的查找,比如 SFO TO ATL 或 RDU TO ATL。(不支持多个跃点。)在搜索框为空的时候单击搜索按钮,会显示所有航班的各种统计数据。您可以使用 near 运算符查找邻近的机场,比如 near:SFO 或 near:SFO TO ATL。查找各种旅行距离可能的晚点(小于 500 英里、500 到 1000 英里、1000 到 2000 英里、2000 及更远),就像 likely:SFO 中一样。任何提供给 Solr 的 /travel 请求处理程序的任意 Solr 查询,比如 &q=AirportCity:Francisco。
[size=1.166em]上面列表中前 3 种查询类型都属于同一种类型的变体。这些变体展示了 Solr 的中心点分面功能,举例而言,显示每条路线、每个航空公司、每个航班编号最常见的抵达晚点时间(比如 SFO TO ATL)。near 选项利用新的 Lucene 和 Solr 空间功能执行大大增强的空间计算,比如复杂多边形交集。likely 选项展示了 Solr 的分组功能,以显示距离某个起飞机场的一定范围内的抵达晚点超过 30 分钟的机场。所有这些请求类型都通过少量的 D3 JavaScript 来显示信息,这增强了地图功能。对于列表中的最后一种请求类型,我只返回了关联的 JSON。这种请求类型支持您自行浏览该数据。如果在您自己的应用程序中使用这种请求类型,那么您自然地想要采用某种特定于应用程序的方式使用响应。[size=1.166em]现在请自行尝试一些查询。例如,如果搜索 SFO TO ATL,您应看到类似图 3 的结果:图 3. 示例 SFO TO ATL 屏幕[size=1.166em]在 图 3 中,地图左侧突出显示了两个机场。右侧的 Route Stats 列表显示了每个航空公司的每个航班最常见的抵达晚点时间。(我只加载了 1987 年的数据。)例如,该列表会告诉您,Delta 航班 156 有 5 次晚点五分钟到达亚特兰大,而且有 4 次提前 6 分钟到达。[size=1.166em]您可以在浏览器的控制台(比如在 Mac 上的 Chrome 中,选择 View -> Developer -> Javascript Console)和在 Solr 日志中查看基础的 Solr 请求。我使用的 SFO-TO-ATL 请求(在这里为了格式化用途而分为 3 行)是:/solr/collection1/travel?&wt=json&facet=true&facet.limit=5&fq=Origin:SFO AND Dest:ATL&q=*:*&facet.pivot=UniqueCarrier,FlightNum,ArrDelay&f.UniqueCarrier.facet.limit=10&f.FlightNum.facet.limit=10
[size=1.166em]facet.pivot 参数提供了此请求中的关键功能。facet.pivot 从航空公司(称为 UniqueCarrier)移动到 FlightNum,再到 ArrDelay,因此提供了 图 3 的 Route Stats 中显示的嵌套结构。[size=1.166em]如果尝试一次 near 插入,如 near:JFK 中所示,您的结果将类似于图 4:图 4. 示例屏幕显示了 JFK 附近的机场[size=1.166em]支持 near 查询的 Solr 请求利用了 Solr 新的空间功能,本文后面将会详细介绍这项功能。至于现在,您可能已通过查看请求本身(这里出于格式化用途而进行了精减)而认识到这项新功能的强大之处:...&fq=source:Airports&q=AirportLocationJTS:"IsWithin(Circle(40.639751,-73.778925 d=3))"...
[size=1.166em]您可能已经猜到,该请求查找以纬度 40.639751 和经度 -73.778925 为中心,以 3 度(大约为 111 千米)为半径的圆圈内的所有机场。[size=1.166em]现在您应很好地认识到 Lucene 和 Solr 应用程序能够以有趣的方式对数据(数字、文本或其他数据)执行切块和切片。而且因为 Lucene 和 Solr 都是开源的,所以可以使用适合商用的许可,您可以自由地添加自己的自定义。更妙的是,4.x 版的 Lucene 和 Solr 增加了您可以插入自己的想法和功能的位置数量,您无需大动干戈修改代码。在接下来查看 Lucene 4(编写本文时最新版为 4.4)的一些要点功能和随后查看 Solr 4 要点功能时,请记住这些功能。

回复

使用道具 举报

千问 | 2007-1-24 12:56:49 | 显示全部楼层
Lucene 4:下一代搜索和分析的基础[size=0.8em]一次巨变[size=1.166em]Lucene 4 几乎完全重写了 Lucene 的支柱功能,以实现更高的性能和灵活性。与此同时,这个版本还代表着社区开发软件的方式上的一次巨变,这得益于 Lucene 新的随机化单元测试框架,以及与性能相关的严格的社区标准。例如,随机化测试框架(可作为一个套装工件供任何人使用)使项目能够轻松测试变量之间的交互,这些变量包括 JVM、语言环境、输入内容和查询、存储格式、计分公式,等等。(即使您绝不使用 Lucene,也会发现该测试框架在您自己的项目中很有用。)
[size=1.166em]对 Lucene 的一些重要的增补和更改涉及到速度和内存、灵活性、数据结构和分面等类别。(要了解 Lucene 中的变化的所有详细信息,请查阅每个 Lucene 发行版中包含的 CHANGES.txt 文件。)速度和内存[size=1.166em]尽管之前的 Lucene 版本被普遍认为已足够快(具体来讲,该速度是相对于类似的一般用途搜索库而言),但 Lucene 4 中的增强使得许多操作比以前的版本快得多。[size=1.166em]图 5 中的图表采集了 Lucene 索引的性能(以 GB 每小时来度量)。(感谢 Lucene 提交者 Mike McCandless 提供的夜间 Lucene 基础测试图表;请参见 参考资料。)图 5 表明,在 [[?年]] 5 月的前半个月发生了巨大的性能改进:图 5. Lucene 索引性能[size=0.8em]不再是过去的 Lucene[size=1.166em]Lucene 4 包含重大的 API 更改和增强,这些都对该引擎有益,而且最终使您能够执行许多新的和有趣的功能。但从之前的 Lucene 版本升级可能需要做大量工作,尤其在您使用了任何更低级或 “专家” API 的时候。(IndexWriter 和 IndexReader 等类仍然在以前的版本中得到了广泛认可,但举例而言,您访问检索词矢量的方式已发生显著变化。)制定相应的计划。
[size=1.166em]图 5 显示的改进来自对 Lucene 构建其索引结构和方式和它在构建它们时处理并行性的方式所做的一系列更改(以及其他一些更改,包括 JVM 更改和固态驱动器的使用)。这些更改专注于在 Lucene 将索引写入磁盘时消除同步;有关的详细信息(不属于本文的介绍范畴),请参阅 参考资料,以获取 Mike McCandless 的博客文章的链接。[size=1.166em]除了提高总体索引性能之外,Lucene 4 还可执行近实时 (NRT) 的索引操作。NRT 操作可显著减少搜索引擎反映索引更改所花的时间。要使用 NRT 操作,必须在您应用程序中,在 Lucene 的 IndexWriter 和 IndexReader 之间执行一定的协调。清单 1(来自下载包的 src/main/java/IndexingExamples.java 文件的一个代码段)演示了这种相互作用:清单 1. Lucene 中的 NRT 搜索示例...doc = new HashSet[I]();index(writer, doc);//Get a searcherIndexSearcher searcher = new IndexSearcher(DirectoryReader.open(directory));printResults(searcher);//Now, index one more docdoc.add(new StringField("id", "id_" + 100, Field.Store.YES));doc.add(new TextField("body", "This is document 100.", Field.Store.YES));writer.addDocument(doc);//The results are still 100printResults(searcher);//Don't commit; just open a new searcher directly from the writersearcher = new IndexSearcher(DirectoryReader.open(writer, false));//The results now reflect the new document that was addedprintResults(searcher);...
[size=1.166em]在 清单 1 中,我首先为一组文档建立了索引,并将提交到 Directory,然后搜索该 Directory — Lucene 中的传统方法。在我继续为另一个文档建立索引时,NRT 就会派上用场:无需执行全面提交,Lucene 从 IndexWriter 创建一个新 IndexSearcher,然后执行搜索。要运行此示例,可将目录更改为 $SOLR_AIR 目录并执行以下命令序列:ant compilecd build/classesjava -cp ../../lib/*:.IndexingExamples
[size=1.166em]备注:我将本文的多个代码示例分组到 IndexingExamples.java 中,所以您可使用同一个命令序列运行清单 2 和清单 4 中的示例。[size=1.166em]打印到屏幕的输出为:...Num docs: 100Num docs: 100Num docs: 101...
[size=1.166em]Lucene 4 还包含内存改进,利用了一些更高级的数据结构(我将在 有限状态自动机和其他好功能 中进行更详细的介绍)。这些改进不仅减少了 Lucene 的内存占用,还大大加快了基于通配符和正则表达式的查询速度。此外,代码库从处理 Java String 对象转变为管理字节数组的大量分配。(BytesRef 类目前在 Lucene 中似乎随处可见。)结果,String 开销减小了,Java 堆上的对象数量得到了更好的控制,这减少了导致所有工作停止的垃圾收集的发生几率。[size=1.166em]一些 灵活性增强 还带来了性能和存储改进,因为您可为您的应用程序使用的数据类型选择更好的数据结构。例如,您接下来将会看到,可在 Lucene 中选择一种方式来索引/存储惟一键(它们是稠密的且没有很好地压缩),选择一种更适合文本的稀疏性的完全不同的方式来索引/存储文本。灵活性[size=1.166em]Lucene 4.x 中的灵活性改进为想要从 Lucene 中榨取最后一点质量和性能的开发人员(和研究人员)提供了大量的机会。为了增强灵活性,Lucene 提供了两个新的明确定义的插件点。两个插件点都显著影响着开发和使用 Lucene 的方式。[size=0.8em]什么是分段?[size=1.166em]Lucene 分段是整个索引的一个子集。从许多方面来看,分段是一个自成一体的微型索引。Lucene 使用了分段平衡索引的搜索可用性和写入速度,以便构建其索引。分段是索引期间只需编写一次的文件,在写入期间,每次提交时都会创建一个新分段。在后台,默认情况下,Lucene 会定期将较小的分段合并到已交到的分段中,以提高读取性能和减少系统开销。您可练习完全掌控这一过程。
[size=1.166em]第一个新插件点设计用来为您提供对 Lucene 分段 的编码和解码的深入控制。Codec 类定义了这项功能。Codec 为您提供了控制帖子列表的格式(也就是倒排索引)、Lucene 存储、加权因子(也称为范数 (norm))等的能力。[size=1.166em]在一些应用程序中,您可能想要实现自己的 Codec。但您可能更想要更改用于索引中的一个文档字段子集的 Codec。为了理解这一点,考虑您放入应用程序中的数据类型可能会对您有所帮助。例如,标识字段(例如您的主键)通常是惟一的。因为主键在一个文档中仅出现一次,所以您可能希望采用与编码文章正文文本不同的方式对它们进行编码。在这些情况下,您不会实际更改 Codec。相反,您更改的是 Codec 所委托的一个更低级类。[size=1.166em]为了演示之目的,我将展示一个代码示例,其中使用了我最喜爱的 CodecSimpleTextCodec。SimpleTextCodec 从名称就可以明白其含义:一个用于编码简单文本中的索引的 Codec。(SimpleTextCodec 编写并通过 Lucene 庞大的测试框架的事实,就是对 Lucene 增强的灵活性的见证。)SimpleTextCodec 太大、太慢,不适合在生产环境中使用,但它是了解 Lucene 索引的幕后工作原理的不错方式,这正是我最喜爱它的原因。清单 2 中的代码将一个 Codec 实例更改为 SimpleTextCodec:清单 2. 在 Lucene 中更改 Codec 实例的示例...conf.setCodec(new SimpleTextCodec());File simpleText = new File("simpletext");directory = new SimpleFSDirectory(simpleText);//Let's write to disk so that we can see what it looks likewriter = new IndexWriter(directory, conf);index(writer, doc);//index the same docs as before...
[size=1.166em]通过运行 清单 2 的代码,您会创建一个本地 build/classes/simpletext 目录。要查看 Codec 的实际运用,可更改到 build/classes/simpletext 并在文本编辑器中打开 .cfs 文件。您可看到,.cfs 文件确实是简单文本,就像清单 3 中的代码段一样:清单 3. _0.cfs 纯文本索引文件的部分内容...term id_97doc 97term id_98doc 98term id_99doc 99ENDdoc 0numfields 4field 0name idtype stringvalue id_100field 1name bodytype stringvalue This is document 100....
[size=1.166em]在很大程度上,只有在您处理极大量索引和查询量时,或者如果您是一位喜爱使用裸机的研究人员或搜索引擎内行,更改 Codec 才有用。在这些情况下更改 Codec 之前,请使用实际数据对各种可用的 Codec 执行广泛的测试。Solr 用户可修改简单的配置项来设置和更改这些功能。请参阅 Solr 参考指南,了解更多的细节(参见 参考资料)。[size=1.166em]第二个重要的新插件点让 Lucene 的计分模型变得完全可插拔。您不再局限于使用 Lucene 的默认计分模型,一些批评者声称它太简单了。如果您喜欢的话,可以使用备用的计分模型,比如来自 Randomness 的 BM25 和 Divergence(参见 参考资料),或者您可以编写自己的模型。为什么编写自己的模型?或许您的 “文档” 代表着分子或基因;您想要采用一种快速方式来对它们分进行类,但术语频率和文档频率并不适用。或者您可能想要试验您在一篇研究文章中读到的一种新计分模型,以查看它在您内容上的工作情况。无论原因是什么,更改计分模型都需要您在建立索引时通过 IndexWriterConfig.setSimilarity(Similarity) 方法更改该模型,并在搜索时通过IndexSearcher.setSimilarity(Similarity) 方法更改它。清单 4 演示了对 Similarity 的更改,首先运行一个使用默认 Similarity 的查询,然后使用 Lucene 的 BM25Similarity 重新建立索引并重新运行该查询:清单 4. 在 Lucene 中更改 Similarityconf = new IndexWriterConfig(Version.LUCENE_44, analyzer);directory = new RAMDirectory();writer = new IndexWriter(directory, conf);index(writer, DOC_BODIES);writer.close();searcher = new IndexSearcher(DirectoryReader.open(directory));System.out.println("Lucene default scoring:");TermQuery query = new TermQuery(new Term("body", "snow"));printResults(searcher, query, 10);BM25Similarity bm25Similarity = new BM25Similarity();conf.setSimilarity(bm25Similarity);Directory bm25Directory = new RAMDirectory();writer = new IndexWriter(bm25Directory, conf);index(writer, DOC_BODIES);writer.close();searcher = new IndexSearcher(DirectoryReader.open(bm25Directory));searcher.setSimilarity(bm25Similarity);System.out.println("Lucene BM25 scoring:");printResults(searcher, query, 10);
[size=1.166em]运行 清单 4 中的代码并检查输出。请注意,计分确实不同。BM25 方法的结果是否更准确地反映了一个用户想要的结果集,最终取决于您和您用户的决定。我建议您设置自己的应用程序,让您能够轻松地运行试验。(A/B 测试应有所帮助。)然后不仅对比了 Similarity 结果,还要对比了各种查询结构、Analyzer 和其他许多方面的结果。有限状态自动机和其他好功能[size=1.166em]对 Lucene 的数据结构和算法的全面修改在 Lucene 4 中带来两个特别有趣的改进:DocValues(也称为跨列字段)。有限状态自动机 (FSA) 和有限状态转换器 (Finite State Transducers, FST)。本文剩余内容将二者都称为 FSA。(在技术上,一个 FST 是访问它的节点时的输出值,但这一区别在本文中并不重要。)
[size=1.166em]DocValues 和 FSA 都为某些可能影响您应用程序的操作类型提供了重大的新性能优势。[size=1.166em]在 DocValues 端,在许多情况下,应用程序需要非常快地顺序访问一个字段的所有值。或者应用程序需要对快速查找这些值来进行排序或分面,而不会导致从索引构建内存型版本(这个过程也称为非反转 (un-inverting))的成本。DocValues 设计用来满足以下需求类型。[size=1.166em]一个没有大量通配符或模糊查询的应用程序应该看到使用 FSA 带来的重大性能改进。Lucene 和 Solr 现在支持利用了 FSA 的查询自动建议和拼写检查功能。而且 Lucene 默认的 Codec 显著减少了磁盘和内存空间占用,在幕后使用 FSA 来存储术语字典(Lucene 在搜索期间用于查询术语的结构)。FSA 在语言处理方面拥有许多用途,所以您也可能发现 Lucene 的 FSA 功能对其他应用程序很有益。[size=1.166em]图 6 显示了一个使用单词 moppopmothstarstoptop 以及关联的权重,从 http://examples.mikemccandless.com/fst.py 构建的 FSA。在这个示例中,您可以想象从 moth 等输入开始,将它分解为它的字符 (m-o-t-h),然后按照 FSA 中的弧线运行。图 6. 一个 FSA 示例[size=1.166em]清单 5(摘自本文的示例代码下载中的 FSAExamples.java 文件)显示了使用 Lucene 的 API 构建您自己的 FSA 的简单示例:清单 5. 一个简单的 Lucene 自动化示例String[] words = {"hockey", "hawk", "puck", "text", "textual", "anachronism", "anarchy"};Collection[B] strings = new ArrayList[B]();for (String word : words) {strings.add(new BytesRef(word));}//build up a simple automaton out of several wordsAutomaton automaton = BasicAutomata.makeStringUnion(strings);CharacterRunAutomaton run = new CharacterRunAutomaton(automaton);System.out.println("Match: " + run.run("hockey"));System.out.println("Match: " + run.run("ha"));
[size=1.166em]在 清单 5 中,我从各种单词构建了一个 Automaton 并将它提供给 RunAutomaton。从名称可以看出,RunAutomaton 通过自动化来运行输入,并在这种情况下与从 清单 5 末尾的打印语句中捕获的输入字符串进行匹配。尽管这个示例很普通,但它为理解我将留给读者探索的 Lucene API 中的更多高级功能(和 DocValues)奠定了基础。(请参见 参考资料 以获取相关链接。)分面[size=1.166em]在其核心,分面生成一定数量的文档属性,为用户提供一种缩小其搜索结果的轻松方式,无需他们猜测要向查询中添加哪些关键词。例如,如果有人在一个购物网站上搜索电视,那么分面功能会告诉他们哪些制造商生产了多少种电视型号。分面也常常用于增强基于搜索的业务分析和报告工具。通过使用更高级的分面功能,您为用户提供了以有趣方式对分面进行切片和切块的能力。[size=1.166em]分面很久以来都是 Solr 的标志性特性(自 1.1 版开始)。现在 Lucene 拥有自己独立的分面模块可供 Lucene 应用程序使用。Lucene 的分面模块在功能上没有 Solr 丰富,但它确实提供了一些有趣的权衡。Lucene 的分面模块不是动态的,因为您必须在索引时制定一些分面决策。但它是分层的,而且它没有将字段动态非反转 (un-invert) 到内存中的成本。[size=1.166em]清单 6(包含在示例代码的 FacetExamples.java 文件中)显示了 Lucene 的一些新的分面功能:清单 6. Lucene 分面示例...DirectoryTaxonomyWriter taxoWriter =new DirectoryTaxonomyWriter(facetDir, IndexWriterConfig.OpenMode.CREATE);FacetFields facetFields = new FacetFields(taxoWriter);for (int i = 0; ifacetRequests = new ArrayList();CountFacetRequest home = new CountFacetRequest(new CategoryPath("Home", '/'), 100);home.setDepth(5);facetRequests.add(home);facetRequests.add(new CountFacetRequest(new CategoryPath("Home/Sports", '/'), 10));facetRequests.add(new CountFacetRequest(new CategoryPath("Home/Weather", '/'), 10));FacetSearchParams fsp = new FacetSearchParams(facetRequests);FacetsCollector facetsCollector = FacetsCollector.create(fsp, reader, taxor);searcher.search(new MatchAllDocsQuery(), facetsCollector);for (FacetResult fres : facetsCollector.getFacetResults()) {FacetResultNode root = fres.getFacetResultNode();printFacet(root, 0);}
[size=1.166em]清单 6 中的重要代码(除正常的 Lucene 索引和搜索外)包含在 FacetFields、FacetsCollector、TaxonomyReader 和 TaxonomyWriter类的使用中。FacetFields 在文档中创建了合适的字段条目,在建立索引时可与 TaxonomyWriter 结合使用。在搜索时,可结合使用TaxonomyReader 与 FacetsCollector,以获取每个类别的正确计数。另请注意,Lucene 的分面模块创建了一个辅助索引,要让该索引生效,必须让它与主要索引保持同步。使用您在前面示例的相同命令中使用的顺序来运行 清单 6 的代码,但将 java 命令中的 FacetExamples替换为 IndexingExamples。您应该得到:Home (0.0) Home/Children (3.0)Home/Children/Nursery Rhymes (3.0) Home/Weather (2.0) Home/Sports (2.0)Home/Sports/Rock Climbing (1.0)Home/Sports/Hockey (1.0) Home/Writing (1.0) Home/Quotes (1.0)Home/Quotes/Yoda (1.0) Home/Music (1.0)Home/Music/Lyrics (1.0)...
[size=1.166em]请注意,在这个特定的实现中,我未包含 Home 分面的计数,因为包含它们可能需要很高的成本。该选项可通过设置适当的FacetIndexingParams 来提供支持,这里没有提供有关介绍。Lucene 的分面模块拥有我未介绍的额外功能。您可以查阅 参考资料 中的文章,探索它们本文未涉及的其他新的 Lucene 功能。现在,我们来看一看 Solr 4.x。

回复

使用道具 举报

千问 | 2007-1-24 12:56:49 | 显示全部楼层
结束语
下一代搜索引擎技术为用户提供了决定如何处理他们的数据的权力。本文详细介绍了 Lucene 和 Solr 4 的功能,我还希望您更广泛地了解了搜索引擎如何解决涉及分析和建议的非基于文本的搜索问题。
Lucene 和 Solr 都在不断演变,这得益于一个庞大的维护社区,该社区由 30 多个提交者和数百位贡献者提供支持。该社区正在积极开发两个主要分支:目前官方发布的 4.x 分支和主干 分支,它代表着下一个主要 (5.x) 版本。在官方版本分支上,该社区致力于实现向后兼容性,实现一种专注于当前应用程序的轻松升级的增量开发方法。在主干分支上,该社区在确保与以前版本的兼容性方面受到的限制更少。如果您希望试验 Lucene 或 Solr 中的前沿技术,那么请从 Subversion 或 Git 签出主干分支代码(参见 参考资料)。无论您选择何种路径,您都可以利用 Lucene 和 Solr 实现超越纯文本搜索的基于搜索的强大分析。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

主题

0

回帖

4882万

积分

论坛元老

Rank: 8Rank: 8

积分
48824836
热门排行