Solr对数据库建立索引

以下资料整理自网络,以及查看solr帮助文档。主要分为两部分,第一部分是对《db-data-config.xml》的配置内容的讲解(属于高级内容),第二部分是DataImportHandler(属于基础),第三部分是对db-data-config.xml的进阶

第一部分是对《db-data-config.xml》

query是获取全部数据的SQL
deltaImportQuery是获取增量数据时使用的SQL
deltaQuery是获取pk的SQL
parentDeltaQuery是获取父Entity的pk的SQL

 

Full Import工作原理
执行本Entity的Query,获取所有数据;
针对每个行数据Row,获取pk,组装子Entity的Query;
执行子Entity的Query,获取子Entity的数据。

 

Delta Import工作原理
查找子Entity,直到没有为止;
执行Entity的deltaQuery,获取变化数据的pk;
合并子Entity parentDeltaQuery得到的pk;
针对每一个pk Row,组装父Entity的parentDeltaQuery;
执行parentDeltaQuery,获取父Entity的pk;
执行deltaImportQuery,获取自身的数据;
如果没有deltaImportQuery,就组装Query

 

限制
子Entity的query必须引用父Entity的pk
子Entity的parentDeltaQuery必须引用自己的pk
子Entity的parentDeltaQuery必须返回父Entity的pk
deltaImportQuery引用的必须是自己的pk

 

第二部分是DataImportHandler

关于DataImportHandler的具体使用方法,详见下文,如果你英文超级好,那看这个链接吧:http://wiki.apache.org/solr/DataImportHandler

 

  大多数的应用程序将数据存储在关系数据库、xml文件中。对这样的数据进行搜索是很常见的应用。所谓的DataImportHandler提供一种可配置的方式向solr导入数据,可以一次全部导入,也可以增量导入。

      概览

         目

  •      能够读取关系数据库中的数据。
  •      通过可配置的方式,能够将数据库中多列、多表的数据生成solr文档  
  •      能够通过solr文档更新solr
  •      提供 通过配置文件就能够导入所有数据的能力
  •       能够发现并处理 由insert、update带来的变化(我们假定在表中有一个叫做“last-modified的列”)
  •       能够配置 “完全导入”和“增量导入”的时间
  •       让读取xml文件,并建立索引成为可配置。
  •       能够将 其他的数据源(例如:ftp,scp,etc)或者其他格式的文档(Json,csv)以插件的形式集成到项目中。

          设计思路

           这个Handler首先要在solrconfig.xml文件中配置下,如下所示。

 

<requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler">
    <lst name="defaults">
      <str name="config">/home/username/data-config.xml</str>      
    </lst>
  </requestHandler>

 

从它的名字上,我们或许也可以猜到,
DataImportHandler正是requestHandler的实现。我们一共需要在两个地方配置文件中进行一些配置。

  • solrconfig.xml 。 data-config.xml必须在这个文件中配置,datasource也可以。不过,一般将datasource放在data-config.xml文件中。
  • data-config.xml
  1.  
    1.    怎样获取数据?(查询语句、url等等)
    2.   要读什么样的数据(关系数据库中的列、或者xml的域)
    3.    做什么样的处理(修改/添加/删除)

      跟关系数据库一起使用

             下面几个步骤是必要的.

  •      定义一个data-config.xml 文件,并这个它的路径配置到solrconfig.xml 中关于DataImportHandler的配置中。
  •       给出Connection的信息(假设你选择在solrconfig中配置datasource)
  • 打开DataImportHandler页面去验证,是否该配置的都配置好了。http://localhost:8983/solr/dataimport
  • 使用“完全导入”命令将数据从数据库中导出,并提交给solr建立索引
  • 使用“增量导入”命令对数据库发生的变化的数据导出,并提交给solr建立索引。

           配置数据源

        将dataSource标签直接添加到dataConfig下面,即成为dataConfig的子元素.

 

<dataSource type="JdbcDataSource" driver="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost/dbname" user="db_username" password="db_password"/>

 

数据源也可以配置在solrconfig.xml中

  • 属性type 指定了实现的类型。它是可选的。默认的实现是JdbcDataSource。
  • 属性 name  是datasources的名字,当有多个datasources时,可以使用name属性加以区分
  • 其他的属性都是随意的,根据你使用的DataSource实现而定。
  • 当然 你也可以实现自己的DataSource。
  •           多数据

         一个配置文件可以配置多个数据源。增加一个dataSource元素就可以增加一个数据源了。name属性可以区分不同的数据源。如果配置了多于一个的数据源,那么要注意将name配置成唯一的。

       例如:

<dataSource type="JdbcDataSource" name="ds-1" driver="com.mysql.jdbc.Driver" url="jdbc:mysql://db1-host/dbname" user="db_username" password="db_password"/>

<dataSource type="JdbcDataSource" name="ds-2" driver="com.mysql.jdbc.Driver" url="jdbc:mysql://db2-host/dbname" user="db_username" password="db_password"/>

 然后这样使用    ..

<entity name="one" dataSource="ds-1" ...>

   ..

</entity>

<entity name="two" dataSource="ds-2" ...>

   ..

</entity>

..
配置JdbcDataSource

JdbcDataSource中的属性有
  • driver(必需的):jdbc驱动名称
  • url(必需的):jdbc链接
  • user:用户名
  • password:密码
  • 批量大小:jdbc链接中的批量大小

任何其他的在JdbcDataSource中配置的属性,都会被直接传给jdbc driver

配置data-config.xml

   solr document是schema,它的域上的值可能来自于多个表.

   data-config.xml的根元素是document。一个document元素代表了一种文档。一个document元素中包含了一个或者多个root实体。一个root实体包含着一些子实体,这些子实体能够包含其他的实体。实体就是,关系数据库上的表或者视图。每个实体都能够包含多个域,每个域对应着数据库返回结果中的一列。域的名字跟列的名字默认是一样的。如果一个列的名字跟solr field的名字不一样,那么属性name就应该要给出。其他的需要的属性在solrschema.xml文件中配置。

    为了能够从数据库中取得想要的数据,我们的设计支持标准sql规范。这使得用户能够使用他任何想要的sql语句。root实体是一个中心表,使用它的列可以把表连接在一起。

     dataconfig的结构

    dataconfig的结构不是一成不变的,entity和field元素中的属性是随意的,这主要取决于processor和transformer。

      以下是entity的默认属性

  •  name(必需的):name是唯一的,用以标识entity
  • processor:只有当datasource不是RDBMS时才是必需的。默认值是SqlEntityProcessor
  • transformer:转换器将会被应用到这个entity上,详情请浏览transformer部分。
  • pk:entity的主键,它是可选的,但使用“增量导入”的时候是必需。它跟schema.xml中定义的uniqueKey没有必然的联系,但它们可以相同。
  • rootEntity:默认情况下,document元素下就是根实体了,如果没有根实体的话,直接在实体下面的实体将会被看做跟实体。对于根实体对应的数据库中返回的数据的每一行,solr都将生成一个document。

     一下是SqlEntityProcessor的属性

  • query (required) :sql语句

  • deltaQuery : 只在“增量导入”中使用

  • parentDeltaQuery : 只在“增量导入”中使用

  • deletedPkQuery : 只在“增量导入”中使用

  • deltaImportQuery : (只在“增量导入”中使用) . 如果这个存在,那么它将会在“增量导入”中导入phase时代替query产生作用。这里有一个命名空间的用法${dataimporter.delta.}详情请看solr1.4.

 

Commands

The handler 通过httprequest 向外界提供它的API . 以下是一些或许你会用到的操作

  • full-import : "完全导入"这个操作可以通过访问URL http://:/solr/dataimport?command=full-import 完成。

    • 这个操作,将会新起一个线程。response中的attribute属性将会显示busy。

    • 这个操作执行的时间取决于数据集的大小。

    • 当这个操作运行完了以后,它将在conf/dataimport.properties这个文件中记录下这个操作的开始时间

    • 当“增量导入”被执行时,stored timestamp这个时间戳将会被用到

    • solr的查询在“完全导入”时,不是阻塞的

    • 它还有下面一些参数:

      • clean : (default 'true'). 决定在建立索引之前,删除以前的索引。

      • commit: (default 'true'). 决定这个操作之后是否要commit

      • optimize: (default 'true'). 决定这个操作之后是否要优化。

      • debug : (default false). 工作在debug模式下。详情请看 the interactive development mode (see here)

  • delta-import : 当遇到一些增量的输入,或者发生一些变化时使用`DataImport - 航梦 - 火星?地球? http://:/solr/dataimport?command=delta-import . 它同样支持  clean, commit, optimize and debug 这几个参数.

  • status : 想要知道命令执行的状态 , 访问 URL http://:/solr/dataimport .它给出了关于文档创建、删除,查询、结果获取等等的详细状况。

  • reload-config : 如果data-config.xml已经改变,你不希望重启solr,而要重新加载配置时,运行一下的命令http://:/solr/dataimport?command=reload-config

  • abort : 你可以通过访问 url http://:/solr/dataimport?command=abort 来终止一个在运行的操作

 

Full Import 例子

This is a relational model of the same schema that Solr currently ships with. 我们使用这个例子来为我们的DataImportHandler建data-config.xml。 我们已经使用这个结构在HSQLDB上建立了一个数据库. 好,现在开始了, 跟着下面的步骤走: 

  1. 下载 example-solr-home.jar 并使用 jar解压  jar -xvf example-solr-home.jar ,解压到你的本地系统. 这个jar文件包含了一个完整的solrhome(里面的配置文件很齐全了)和一个RSS的例子。它也包含了一个hssqldb数据库的例子.

  2. 在 example-solr-home目录, 这里有一个 solr.war. 拷贝 这个 war 文件到你的 tomcat/jetty webapps 文件夹.  这个 war file 也包含了hsqldb的JDBC driver. 如果你想在你已经有了的solr项目中部署,你只需要将  'dataimport.jar' 拷贝到 你的solr项目的 WEB-INF/lib 目录下。

  3. 使用example-data-config目录下的solr目录作为你solrhome

  4. 访问 DataImport - 航梦 - 火星?地球? http://localhost:8983/solr/dataimport 验证一下配置

  5. 访问 DataImport - 航梦 - 火星?地球? http://localhost:8983/solr/dataimport?command=full-import 执行一个“完全导入”

上面给出的solr目录是一个多核的solr home。它有两个核,一个是DB example,一个是RSSexample(新属性)。

这个例子的data-config.xml 如下:

<dataConfig>

<dataSource driver="org.hsqldb.jdbcDriver" url="jdbc:hsqldb:/temp/example/ex" user="sa" />

    <document name="products">

        <entity name="item" query="select * from item">

            <field column="ID" name="id" />

            <field column="NAME" name="name" />

            <field column="MANU" name="manu" />

            <field column="WEIGHT" name="weight" />

            <field column="PRICE" name="price" />

            <field column="POPULARITY" name="popularity" />

            <field column="INSTOCK" name="inStock" />

            <field column="INCLUDES" name="includes" />



            <entity name="feature" query="select description from feature where item_id='${item.ID}'">

                <field name="features" column="description" />

            </entity>

            <entity name="item_category" query="select CATEGORY_ID from item_category where item_id='${item.ID}'">

                <entity name="category" query="select description from category where id = '${item_category.CATEGORY_ID}'">

                    <field column="description" name="cat" />

                </entity>

            </entity>

        </entity>

    </document>

</dataConfig>

这里, 根实体是一个名叫“item”的表,它的主键是id。我们使用语句 "select * from item"读取数据. 每一项都拥有多个特性。看下面feature实体的查询语句

<entity name="feature" query="select description from feature where item_id='${item.id}'">

       <field name="feature" column="description" />

   </entity> 

feature表中的外键item_id跟item中的主键连在一起从数据库中取得该row的数据。相同地,我们将item和category连表(它们是多对多的关系)。注意,我们是怎样使用中间表和标准sql连表的

<entity name="item_category" query="select category_id from item_category where item_id='${item.id}'">
                <entity name="category" query="select description from category where id = '${item_category.category_id}'">
                    <field column="description" name="cat" />
                </entity>
            </entity>

短一点的 data-config

在上面的例子中,这里有好几个从域到solr域之间的映射。如果域的名字和solr中域的名字是一样的话,完全避免使用在实体中配置域也是可以的。当然,如果你需要使用转换器的话,你还是需要加上域实体的。

下面是一个更短的版本

<dataConfig>

    <dataSource driver="org.hsqldb.jdbcDriver" url="jdbc:hsqldb:/temp/example/ex" user="sa" />

    <document>

        <entity name="item" query="select * from item">                    

            <entity name="feature" query="select description as features from feature where item_id='${item.ID}'"/>            

            <entity name="item_category" query="select CATEGORY_ID from item_category where item_id='${item.ID}'">

                <entity name="category" query="select description as cat from category where id = '${item_category.CATEGORY_ID}'"/>                        

            </entity>

        </entity>

    </document>

</dataConfig>

使用“增量导入”命令

你可以通过访问URL DataImport - 航梦 - 火星?地球? http://localhost:8983/solr/dataimport?command=delta-import 来使用增量导入。操作将会新起一个线程,response中的属性statue也将显示busy now。操作执行的时间取决于你的数据集的大小。在任何时候,你都可以通过访问   http://localhost:8983/solr/dataimport 来查看状态。

当 增量导入被执行的时候,它读取存储在conf/dataimport.properties中的“start time”。它使用这个时间戳来执行增量查询,完成之后,会更新这个放在conf/dataimport.properties中的时间戳。

Delta-Import 例子

我们将使用跟“完全导入”中相同的数据库。注意,数据库已经被更新了,每个表都包含有一个额外timestamp类型的列  叫做last_modified。或许你需要重新下载数据库,因为它最近被更新了。我们使用这个时间戳的域来区别出那一行是上次索引以来有更新的。

看看下面的这个 data-config.xml

<dataConfig>

    <dataSource driver="org.hsqldb.jdbcDriver" url="jdbc:hsqldb:/temp/example/ex" user="sa" />

    <document name="products">

            <entity name="item" pk="ID" query="select * from item"

                deltaQuery="select id from item where last_modified > '${dataimporter.last_index_time}'">           



            <entity name="feature" pk="ITEM_ID" 

                    query="select description as features from feature where item_id='${item.ID}'">                

            </entity>

            <entity name="item_category" pk="ITEM_ID, CATEGORY_ID"

                    query="select CATEGORY_ID from item_category where ITEM_ID='${item.ID}'">

                <entity name="category" pk="ID"

                       query="select description as cat from category where id = '${item_category.CATEGORY_ID}'">                    

                </entity>

            </entity>

        </entity>

    </document>

</dataConfig>

注意到item实体的 属性deltaquery了吗,它包含了一个能够查出最近更新的sql语句。注意,变量{dataimporter.last_index_time} 是DataImporthandler传过来的变量,我们叫它时间戳,它指出“完全导入”或者“部分导入”的最后运行时间。你可以在data-config.xml文件中的sql的任何地方使用这个变量,它将在processing这个过程中被赋值。

注意

  • 上面例子中deltaQuery 只能够发现item中的更新,而不能发现其他表的。你可以像下面那样在一个sql语句中指定所有的表的更新。这里要特别说明一下的就是,它的细节对于一个使用者来说是一个不错的练习。

deltaQuery="select id from item where id in

                                (select item_id as id from feature where last_modified > '${dataimporter.last_index_time}')

                                or id in

                                (select item_id as id from item_category where item_id in

                                    (select id as item_id from category where last_modified > '${dataimporter.last_index_time}')

                                or last_modified > '${dataimporter.last_index_time}')

                                or last_modified > '${dataimporter.last_index_time}'"

  • 写一个类似上面的庞大的deltaQuery 并不是一件很享受的工作,我们还是选择其他的方法来达到这个目的

 

<dataConfig>

    <dataSource driver="org.hsqldb.jdbcDriver" url="jdbc:hsqldb:/temp/example/ex" user="sa" />

    <document>

            <entity name="item" pk="ID" query="select * from item"

                deltaQuery="select id from item where last_modified > '${dataimporter.last_index_time}'">

                <entity name="feature" pk="ITEM_ID" 

                    query="select DESCRIPTION as features from FEATURE where ITEM_ID='${item.ID}'"

                    deltaQuery="select ITEM_ID from FEATURE where last_modified > '${dataimporter.last_index_time}'"

                    parentDeltaQuery="select ID from item where ID=${feature.ITEM_ID}"/>

                

            

            <entity name="item_category" pk="ITEM_ID, CATEGORY_ID"

                    query="select CATEGORY_ID from item_category where ITEM_ID='${item.ID}'"

                    deltaQuery="select ITEM_ID, CATEGORY_ID from item_category where last_modified > '${dataimporter.last_index_time}'"

                    parentDeltaQuery="select ID from item where ID=${item_category.ITEM_ID}">

                <entity name="category" pk="ID"

                        query="select DESCRIPTION as cat from category where ID = '${item_category.CATEGORY_ID}'"

                        deltaQuery="select ID from category where last_modified > '${dataimporter.last_index_time}'"

                        parentDeltaQuery="select ITEM_ID, CATEGORY_ID from item_category where CATEGORY_ID=${category.ID}"/>

            </entity>

        </entity>

    </document>

</dataConfig>

   

除了根实体(有两个)以外,这里一共有三个查询,每个实体个一个。

查询语句,为我们取得需要建立索引的数据。

  • deltaQuery 取得从上次索引更新时间以来有更新的实体的主键。

  • parentDeltaQuery 从deltaQuery中取得当前表中更新的行,并把这些行提交给父表。因为,当子表中的一行发生改变时,我们需要更新它的父表的solr文档。

下面是一些值得注意的地方:

  • 对于query语句返回的每一行,子实体的query都将被执行一次

  • 对于deltaQuery返回的每一行,parentDeltaQuery都将被执行。

  • 一旦根实体或者子实体中的行发生改变,我们将重新生成包含该行的solr文档。

 XML/HTTP Datasource使用指南

DataImportHandler 能够帮我们为基于HTTP的数据源建立索引. 目前支持REST/XML APIs 和RSS/ATOM Feeds.

配置HttpDataSource

HttpDataSource在dataconfig.xml中的配置看起来应该像这样: 

  <dataSource type="HttpDataSource" baseUrl="http://host:port/" encoding="UTF-8" connectionTimeout="5000" readTimeout="10000"/>  

属性:

  • baseUrl (可选): 在Dev/QA/Prod 环境中,host/port改变时,你会用到它。使用这个属性,你可以找出配置到solrconfig.xml的变化。

  • encoding(可选): 默认情况下,encoding是response 头使用的encoding.你可以使用这个属性去覆盖默认值。

  • connectionTimeout (可选):默认值是5000ms

  • readTimeout (可选): 默认值是10000ms

在 data-config.xml中的配置

 一个 xml/http data source中的实体有下面一些属性,也可以有上面提到的默认属性。

  • processor (必需的) : 它的值应该是"XPathEntityProcessor"

  • url (必需的) :  REST API要使用这个api. (能够被模板化). 假设数据源是一个文件,那么url应该是这个文件的位置。

  • stream (可选) : 如果xml很大,那么它应该设为true

  • forEach(必需的) : xpath表达式,通过这个表达式可以取得想要的值。 如果这里有多个想要的值,那么将xpath表达式用“|”分开。如果useSolrAddSchema设为true的话,这个是可以被忽略的。

  • xsl(可选):使用xsl对xml进行预处理。你需要提供一个文件系统的全路径,或者一个url。

  • useSolrAddSchema(可选): Set it's value to 'true' if the xml that is fed into this processor has the same schema as that of the solr add xml. No need to mention any fields if it is set to true.

域能够有以下这些属性 (此外还有那些默认值):

  • xpath (必需的) : 记录中的一列,也就是域的xpath表达式 . 如果 该域并不来自任何的一个单一的xml属性,xpath是可以被忽略的. 我们可以通过转化器来使用多个xml属性来合成该域。如果一个域被声明成多值的,如果xpath表达式生成的也是多值的,那么XPathEntityProcessor将会自动处理它,而不需要我们做额外的工作。

  • commonField : 能够被设为 (true或者false),假设这个是true值,一旦一个记录中有这样的域,那么其他记录被写索引的时候,这个域也会跟着记录被写到索引里面。

如果 一个API支持分块数据(当一个数据集太大时),可能需要多次调用才能完成这个处理过程。XPathEntityprocessor 通过转换器支持这个特性。如果转换器返回的的行带有属性“hasMore”,并且这个属性的值等于true,那么Processor 将会使用同样的url模板发出令一次请求(实际的url是需要重新计算的)。一个转换器也可以传递一个完整的url路径,这个url被包含在属性“nextUrl”中,nextUrl的值必需是一个完整的url。

XPathEntityProcessor 通过实现streaming parser来支持取得xpath子集的操作。完整的xpath是不被支持的,但是常见的应用都是受支持的。

HttpDataSource 例子

下载 在DB 部分中的“完全导入”例子,试着去体验一下。我们将在这里例子中为slashotRSS建立索引。

这个例子的data-config配置看起来像这样。

<dataConfig>

        <dataSource type="HttpDataSource" />

        <document>

                <entity name="slashdot"

                                pk="link"

                                url="http://rss.slashdot.org/Slashdot/slashdot"

                                processor="XPathEntityProcessor"

                                forEach="/RDF/channel | /RDF/item"

                                transformer="DateFormatTransformer">

                                

                        <field column="source" xpath="/RDF/channel/title" commonField="true" />

                        <field column="source-link" xpath="/RDF/channel/link" commonField="true" />

                        <field column="subject" xpath="/RDF/channel/subject" commonField="true" />

                        

                        <field column="title" xpath="/RDF/item/title" />

                        <field column="link" xpath="/RDF/item/link" />

                        <field column="description" xpath="/RDF/item/description" />

                        <field column="creator" xpath="/RDF/item/creator" />

                        <field column="item-subject" xpath="/RDF/item/subject" />

                        <field column="date" xpath="/RDF/item/date" dateTimeFormat="yyyy-MM-dd'T'hh:mm:ss" />

                        <field column="slash-department" xpath="/RDF/item/department" />

                        <field column="slash-section" xpath="/RDF/item/section" />

                        <field column="slash-comments" xpath="/RDF/item/comments" />

                </entity>

        </document>

</dataConfig>


这个data-config有很多值得借鉴的地方。 我建议你看下SlashdotRSS的结构图,它有一些头部元素,例如title、link、subject。这些元素将分别通过xpath语法映射到source、source-link、subject这些solr域。这个种子有多个item元素,这些元素包含了真正的新闻信息。所以,我们希望做得是,为每一个item元素建立一个文档。

XPathEntityprocessor 是一行一行的读取xml文件的(这里的行指的是一个xml元素)。它使用属性“forEach”去识别每一行 。在这个例子一种“forEach”的值是'/RDF/channel | /RDF/item'。也就是说这个xml有两种类型的行(这里使用一个OR的xpath语法,用以支持多个类型) 。当遇到一个行的时候,它会在行的域声明中读取尽量多的域。在这个例子中,当它读到行“/RDF/channel”时,它将会得到3个域。它处理完这个行的时候,它就会意识到,这个行并没有pk这个域的值,于是它并不会试图去建立一个solr文档(即使它去做,它也会失败)。但是这个三个域都有一个属性commonField ,并且它的值是true,所以它将会保留这个域的值,以便后面的行可以使用

它继续前进,然后遇到/RDF/item ,接着一个个处理这些行。它将会取得除了那个三个域之外的所有域。但是因为他们是common field。处理器会把公共域也加到这个记录中,然后写入索引。

transformer=DateFormatTransformer 又是什么呢?你可以看一下DateFormatTransformer有关部分。

你可以使用这些特性来从REST API ,例如 rss、atom、xml、其他solr服务器、甚至是格式良好的xhtml文档,建立索引。我们的xpath语法有它自己的限制(不支持通配符,只可以是全路径),但是一般的应用是绝对没有问题的,而且它是基于streaming parser的,它非常快,并且在读取非常大的xml文件的时候,它的内存消耗始终保持如一。它不支持命名空间,它却可以处理带有命名空间的xml文件。当你处理带有命名空间的xpath的时候,你需要做的是,丢弃命名空间部分,只留下其他的部分(例如,这个标签,相对应的xpath部分是subject)。很容易,是吧?而且你不需要写一行代码,好好享受吧。DataImport - 航梦 - 火星?地球?

DataImport - 航梦 - 火星?地球? 注意 : 不像数据库,如果你使用XPathEntityProcessor,想忽略域声明是不可能。域通过你声明的xpaths来从xml中解析相应的数据。

例子: 索引 wikipedia

利用下面的data-config.xml文件可以对wikipedia的数据建立索引。从wikipedia下载下来的pages-articles.xml.bz2文件解压之后大概有18g。

<dataConfig>

        <dataSource type="FileDataSource" encoding="UTF-8" />

        <document>

        <entity name="page" processor="XPathEntityProcessor" stream="true" forEach="/mediawiki/page/" url="/data/enwiki-20080724-pages-articles.xml">

                <field column="id" xpath="/mediawiki/page/id" />

                <field column="title" xpath="/mediawiki/page/title" />

                <field column="revision" xpath="/mediawiki/page/revision/id" />

                <field column="user" xpath="/mediawiki/page/revision/contributor/username" />

                <field column="userId" xpath="/mediawiki/page/revision/contributor/id" />

                <field column="text" xpath="/mediawiki/page/revision/text" />

                <field column="timestamp" xpath="/mediawiki/page/revision/timestamp" />

        </entity>

        </document>

</dataConfig> 


schema.xml中有关的部分如下所示:
<field name="id" type="integer" indexed="true" stored="true" required="true"/>

<field name="title" type="string" indexed="true" stored="false"/>

<field name="revision" type="sint" indexed="true" stored="true"/>

<field name="user" type="string" indexed="true" stored="true"/>

<field name="userId" type="integer" indexed="true" stored="true"/>

<field name="text" type="text" indexed="true" stored="false"/>

<field name="timestamp" type="date" indexed="true" stored="true"/>

<field name="titleText" type="text" indexed="true" stored="true"/>

...

<uniqueKey>id</uniqueKey>

<copyField source="title" dest="titleText"/>



为7278241个文章建立索引大概花了2个小时40分,内存使用量的峰值在4G左右。

使用“增量导入”命令

只有SqlEntitiProcessor支持增量数据!XPathEntityProcessor还没有实现它。所以,不幸运的是,现在还不能为“增量导入”提供支持。如果你想要在XPathEntityProcessor中实现这些方法,你可以在EntityProcessor.java中看看这些方法的解释。

Extending the tool with APIs

我们所展现的例子确实没有多大价值,单靠配置xml文件就满足所有的需求是不可能的。所以我们提供了一些抽象类,可以通过这些方法来提高功能。

Transformer

每一条从数据库中取得的数据能够被直接处理掉,或者通过它创建一个全新的域,它设置能够返回多行数据。配置文件必须像下面那样设置。

<entity name="foo" transformer="com.foo.Foo" ... />

DataImport - 航梦 - 火星?地球? 注意-- trasformer的值必须是一个可以使用的classname。如果class包是'org.apache.solr.handler.dataimport' ,包名可以被忽略。solr.也是可以使用的,如果这个class在solr的一个包下的话。这个规则适应所有的可插入的类,像DataSource、EntityProcessor、Evaluator。 

类Foo必须继承抽象类org.apache.solr.hander.dataimport.Transformer.这个类只有一个抽象方法。

transformer这个属性可以有多个transformers()(比如 transformer="foo.X,foo.Y") 之间用逗号隔开。 transformers 会形成一条处理链。它们将会按照它们的排列顺序起作用。

public abstract class Transformer {

  /**

   * The input is a row of data and the output has to be a new row.

   *

   * @param context The current context

   * @param row     A row of data

   * @return The changed data. It must be a Map if it returns

   *         only one row or if there are multiple rows to be returned it must

   *         be a List

> */ public abstract Object transformRow(Map row, Context context); }

Context 是一个抽象的类,它提供上下文关系,这可能在处理数据的时候要用到。

另外,类Foo,可以选择不不实现这个抽象类,而只需要下面这个方法

public Object transformRow(Map row)

So there is no compile-time dependency on the DataImportHandler API

它的配置是灵活的。它允许用户向标签entity和field提供任意的属性。tool将会读取数据,并将它传给实现类。如果Transformer需要额外的的信息,它可以从context中取得。

正则表达式转换器

tool它提供了一个内嵌的转换器,叫做正则表达式转换器。它可以使用正则表达式从原数据中解析出我们想要的值。org.apache.solr.handler.dataimport.RegexTransformer 是它的名字. 因为它属于默认的包,所以它的包名是可以被忽略的。

例子: 

<entity name="foo" transformer="RegexTransformer"  

query="select full_name , emailids from foo"/>

... />

   <field column="full_name"/>

   <field column="firstName" regex="Mr(/w*)/b.*" sourceColName="full_name"/>

   <field column="lastName" regex="Mr.*?/b(/w*)" sourceColName="full_name"/>

   <field column="mailId" splitBy="," sourceColName="emailids"/>

</entity>

 

属性

RegexTransfromer只对属性中有regex或者splitBy的域起作用。所有的属性我们列在下面。

  • regex : 这是要匹配的正则表达式。regex和splitBy两者必有其一。如果没有,这个域将不会被正则表达式转换器处理。

  • sourceColName : 正则表达式起作用的列。. 如果这个这个属性不存在,那么source将等同域target。

  • splitBy : 如果正则表达式,是被用来分割一个字符串以获得多个值,那么使用这个。

  • replaceWith : 跟 属性regex一起使用。相当于我们平常使用的方法new String().replaceAll(, )

这里,属性‘regex’和‘sourceColName’是转换器自定义的属性。它从resultSet中读取域‘full_name’的值,然后转换它,并将结果分别传给‘firstName’和‘lastName’。所以,尽管查询结果只返回一列“full_name”,但solr document依然可以获得额外的两个域“firstName”和‘lastName’。

域'emailids'  是一个用逗号分隔着的值。 所以,我们最终可以从emailids得到一个以上的emial id。mailid 在solr中应该被定义为多值的。

脚本转换器

你可以使用javascript 或者其他的 脚本语言来写转换器,只要java支持这种脚本。在这里我们应该使用java 6.

<dataConfig>
        <script><![CDATA[
                function f1(row)        {
                    row.put('message', 'Hello World!');
                    return row;
                }
        ]]></script>
        <document>
                <entity name="e" pk="id" transformer="script:f1" query="select * from X">
                ....
                </entity>
        </document>
</dataConfig>

 

  • 你可以在dataConfig结点中设置script 标签。默认的语言是javascript。你当然可以使用另外一种语言,你可以通过script标签中的属性language去设置它。(必须有java6的支持)。

  • 你可以写任意多的转换函数。每个函数必须接受一个相当于 Map的row变量,然后要返回一个row。(转换以后)

  • 通过在实体中指定 transformer=“script:”来使一个实体使用脚本函数。

  • 在上面的data-config中,对于结果中返回的实体e的每一个行,javascript函数都将被执行一次。 

  • 执行机制跟一个java的转换器是一样的。在Transformer 中有两个参数 (transformRow(Map,Context ))。在javascript中,第二个参数被忽略了,但它一样是起作用的。

日期格式转换器

这里有一个内嵌的转换器,叫做DateFormatTransformer(日期格式转换器) ,这个在将字符型时间转换成java.util.Date的类型的时候是很有用的。

<field column="date" xpath="/RDF/item/date" dateTimeFormat="yyyy-MM-dd'T'hh:mm:ss" />

属性

日期格式转换器只对带有属性“dateTimeFormat”的域才起作用。其他属性如下所示。

  • dateTimeFormat : 转换使用的格式。这个必须服从java的SimpleDateformat。

  • sourceColName : 要使用日期转换的列。如果没有设定这个值,那么源列跟目标域的名称是一样的。

上面的域的定义在RSS例子中有使用,以转换RSS种子项中的时间格式。

数字格式转换器

能将一个字符串转换成一个数字,使用的是java中类NumberFormat。例子:

<field column="price" formatStyle="number" />

默认情况下,类Numberformat使用系统的本地格式去转换一个字符串,如果你需要指定一个不同的本地类型的话,你可以像下面这样指定。例子:

<field column="price" formatStyle="number" locale="de-DE" />
属性

数字格式转换器 只对那些带有属性“formatStyle”的域有用。

  • formatStyle : 解析这个域所需要的格式。这个属性的值必须是(number|percent|integer|currency)中的一个。可以参考 java DataImport - 航梦 - 火星?地球? NumberFormat.

  • sourceColName : 要使用数字转换的列。如果没有设定这个值,那么源列跟目标域的名称是一样的。

  • locale : 要转换的字符串所使用的国际化格式。如果没有设定这个值,它的默认值是系统的国际化格式。它的值必须是language-country。例如 en-US。

模板转换器

使用DataImportHandler中强大的模板引擎来创建或者设定一个域的值。例如:

<entity name="e" transformer="TemplateTransformer" ..>

<field column="namedesc" template="hello${e.name},${eparent.surname}" />
...
</entity>

这里模板的规则跟‘query’、‘url’的规则是一样的。它主要能帮我们将多个值连到一起,或者忘域值注入其他的字符。这个转换器只对拥有属性‘template’的域起作用。

属性
  • template : 模板字符串。上面的例子中有两个占位符,‘${e.name}和${eparent.surname}’。 In the above example there are two placeholders '${e.name}' and '${eparent.surname}' . 两个值都必须存在,否则这个模板将不会起作用。

自定义模板转换器

如果你需要在将数据送给solr之前,对数据进行一些处理,你可以写一个你自己的转换器。让我们来看一个例子。在我们的schema中我们有一个单值的域叫做‘artistName’,类型是String。这个域的值包含了多个单词,例如‘Celine Dion’,这里有一个问题 ,这个值包含一些开头空格和结尾空格,这些空格不是我们想要的。solr的WhitespaceAnalyze在这里用不上,因为,我们并不想把这个字符串切词了。一个可以选择的解决方案就是自己写一个TrimTransformer。

一个简单的TrimTransformer

package foo;

public class TrimTransformer    {
        public Object transformRow(Map row)     {
                String artist = row.get("artist");
                if (artist != null)             
                        row.put("ar", artist.trim());
                return row;
        }
}

不需要去继承任何类。这个类只需要有transformRow 方法,就像上面的那样。DataImportHandler会自动辨别它,并使用反射机制来调用它。你可以在你的data-config.xml文件中这样来设置:

<entity name="artist" query="..." transformer="foo.TrimTransformer">

        <field column="artistName" />

</entity>



一个通用的TrimTransformer

假设,你想写一个通用的TrimTransformer,这样你就不用将要处理的列写在的代码里面。这里,我们需要在data-config.xml中设一个标记来表示这个域是否要应用这个转换器。

<entity name="artist" query="..." transformer="foo.TrimTransformer">
        <field column="artistName" trim="true" />
</entity>

现在,你需要去继承 Transformer 这个抽象类,并使用Context中的API来获得实体中的域,并获得域中的属性,检查标记有没有被设值。

package foo;

public class TrimTransformer extends Transformer        {
        public Map transformRow(Map row, Context context) {
                List

> fields = context.getAllEntityFields(); for (Map field : fields) { // Check if this field has trim="true" specified in the data-config.xml String trim = field.get("trim"); if ("true".equals(trim)) { // Apply trim on this fied String columnName = field.get("column"); // Get this field's value from the current row String value = row.get(columnName); // Trim and put the updated value back in the current row if (value != null) row.put(columnName, value.trim()); } } return row; } }

如果域是多值的,那么返回值将会是一个list而不是单单一个对象,而且需要被恰当的处理。你可以将DataImprotHandler打包成一个jar包,然后再扩展Transformer和Context。

EntityProcessor(实体处理器)

默认的情况下,每个实体都会被sqlEntityProcessor处理。在系统 使用RDBMS作为数据源的时候,它很适用。对于其他的数据源,例如 REST 或者不是sql的数据源 ,你可以选择 继承org.apache.solr.handler.dataimport.Entityprocessor. 这个抽象类。它被设计成从实体中一行一行的读取数据。最简单的实现自己的实体处理器的方式是 继承EntityProcessorBase ,然后重写方法 public Map nextRow() method。 'EntityProcessor'依赖于 数据源来获取数据。数据源的返回类型对实体处理器来说是很重要的。下面是一些内嵌的实体处理器。

SqlEntityProcessor

它是默认的,数据源必须是DataSource类型的,在这里默认的情况下使用的是jdbcDataSource。

XPathEntityProcessor

处理XML类型的数据源。数据源的类型必须是DataSource类型的,这种类型的数据源有HttpDataSource和FileDatasource类型。

FileListEntityProcessor

简单的处理器,它能够从文件系统中得到文件的集合。这个系统基于一些标准,它不使用数据源,下面是实体的属性:

  • fileName :(必须) 辨别文件的正则表达式

  • baseDir : (必须) 根目录(虚拟路径)

  • recursive : 是否要递归的获取文件,默认是false。

  • excludes : 匹配文件名的正则表达式

  • newerThan : 一个数字参数 . 使用格式 (yyyy-MM-dd HH:mm:ss) . 它可以是一个datemath 类型的字符串,例如:('NOW-3DAYS'). 需要加单引号。它也可以是一个变量,像${var.name}这样。

  • olderThan : 一个数字参数 . 跟上一条的规则是一样的

  • rootEntity :根实体的值必须是false,除非你想索引文件名。位置直接在下面的是根实体,这就意味着根实体产生的行都将被当成一个document存放在lucene里面。但是,在这个例子里面,我们并不想为每个文件建立一个document,我们想对x实体产生的行建立document,因为实体f的属性rootEntiry等于false,所以在直接位于实体f下面的实体将成为根实体,它所产生的行将会被当成一个document。

  • dataSource :它必须被设为null值,因为这里并不需要使用任何的数据源,即是说,我们将不会创建Datasource的实例。(在大多数的情况下,只有一个数据源,jdbc数据源,所有的实体都用,在这里,数据源是没有必要的。)

例子:

<dataConfig>
    <dataSource type="FileDataSource" />
    <document>
        <entity name="f" processor="FileListEntityProcessor" fileName=".*xml" newerThan="'NOW-3DAYS'" recursive="true" rootEntity="false" dataSource="null">
            <entity name="x" processor="XPathEntityProcessor" forEach="/the/record/xpath" url="${f.fileAbsolutePath}">
                <field column="full_name" xpath="/field/xpath"/> 
            </entity>
        </entity>
    <document>
<dataConfig>

千万要注意rootEntiry这个属性,由这个处理器所产生的域有fileAbsolutePath,fileSize,fileLastModified,fileName.

CachedSqlEntityProcessor

应该说,这是SqlEntityProcessor的一个扩展,这个处理器通过缓存一些行,来减少数据库查询。它几乎对根实体没有用,因为这个实体中只有一个sql语句被执行了。

Example 1.

<entity name="x" query="select * from x">
    <entity name="y" query="select * from y where xid=${x.id}" processor="CachedSqlEntityProcessor">
    </entity>
<entity>

这个例子的用法跟下面的是一样的,一个查询被执行完,它的结果被存储起来,下次这个查询再被执行的的时候,它将会从缓存中取出结果并返回。

Example 2:

<entity name="x" query="select * from x">
    <entity name="y" query="select * from y" processor="CachedSqlEntityProcessor"  where="xid=x.id">
    </entity>
<entity>

这个例子跟前一个的区别在于属性‘where’。这个例子中,查询语句将从表中取回所有的数据,并把他们都放在缓存中。其中的关键就在域 属性‘where’。缓存使用y中的xid作为键值,实体被查询的时候x.id的值就会被计算出来,我们首先会在缓存中找匹配的数据,接着返回。

 

在属性where中,=号之前的值是y中的列,=号之后的值是计算出来的要在缓存中查找的值。

DataSource(数据源)

 org.apache.solr.handler.dataimport.DataSource 能被继承。

public abstract class DataSource {
  /**

   * Initializes the DataSource with the Contextand

   * initialization properties.

   *

* This is invoked by the DataImporter after creating an * instance of this class. * * @param context * @param initProps */ public abstract void init(Context context, Properties initProps); /** * Get records for the given query.The return type depends on the * implementation . * * @param query The query string. It can be a SQL for JdbcDataSource or a URL * for HttpDataSource or a file location for FileDataSource or a custom * format for your own custom DataSource. * @return Depends on the implementation. For instance JdbcDataSource returns * an Iterator> */ public abstract T getData(String query); /** * Cleans up resources of this DataSource after use. */ public abstract void close(); }

 它必须在数据源的定义部分被配置。

<dataSource type="com.foo.FooDataSource" prop1="hello"/>


JdbcdataSource

这个是默认的,它的声明如下:

public class JdbcDataSource extends DataSource>>

 

它可以一条一条的遍历数据库,每一行数据被当作一个Map。

HttpDataSource

XPathEntityProcessor使用这个数据源 . 它的声明如下:

public class HttpDataSource extends DataSource

FileDataSource

这个很像HttpDataSource . 它的声明如下:

public class FileDataSource extends DataSource  

The attributes are:

  • basePath: (可选的)  ,得到所需要的值时必须的基本路径。

  • encoding: (可选的)当文件编码跟平台编码不一样的时候,应当设定这个值。

Boosting , Skipping documents(提高文档的得分,或者跳过文档)

我们还可以在运行的时候提高一个文档的得分,或者跳过某一个特定的文档。

可以通过自定义转化器,增加一个属性,并将它设为true,这样就可以跳过这个文档了。可以通过,增加一个属性docBoost ,属性是文档的评分的这种方式给文档打分。Write a custom Transformer to add a value $skipDoc with a value 'true' to skip that document. To boost a document with a given value add $docBoost with the boost value

在 solrconfig.xml中增加数据源

我们也可以在solrconfig.xml中配置数据源,属性是一样的,只是方式稍微有点不同。 

     
    
      <requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler">
    <lst name="defaults">
      <str name="config">/home/username/data-config.xml</str>
      <lst name="datasource">
         <str name="driver">com.mysql.jdbc.Driver</str>
         <str name="url">jdbc:mysql://localhost/dbname</str>
         <str name="user">db_username</str>
         <str name="password">db_password</str>
      </lst>
    </lst>
  </requestHandler>
    

结构图

下面的这个图显示了一般的配置文件的逻辑流程。

DataImport - 航梦 - 火星?地球?

上面的这个图表达了这样的一个意思:一共有三个数据源,两个关系数据库的数据源,和一个http/xml的数据源。

 

         jdbc1 和jdbc2 是JdbcDataSource ,它配置在solrconfig.xml文件中。

  • http是一个HttpDataSource类型的数据源。

  • 根实体是一个叫做a的表,它使用jdbc1作为它的数据源。实体一般都与表名相同。

  • 实体A有两个子实体 B 和C 。B使用http数据源,C使用jdbc2数据源。

  • 在执行一个full-import的命令的时候,根实体A会首先被执行。

  • 由实体A导出的每一行,都会被传给实体B和实体C。

  • B和C通过占位符来使用实体A中的数据。占位符:${A.a}。

    • B 有一个url属性

    • C 有一个query属性

  • C 有两个转换器 ‘f’和‘g’。

  • 由C产生的每一行数据,都会被有序的传给 'f '和‘g’(转换器是链式的,即有序的)。每个转换器都能够改变输入的值。在这里转换器‘g’将从一行数据(f(c .1))中产生两行数据。 

  • 最近将每个实体的结果合并成为一个文档。

    • 请注意:从C产生的中间结果,例如C.1 c.2 ,f(c.1) f(c.2),都将被忽略掉。

域声明

域的声明,能够帮助我们通过提供一些额外的信息得到那些不能自动获取到的值。它依赖于结果集中的列。在dataConfig里面配置的域,一般情况下应该跟schema配置的一样。它应该自动继承schema.xml中的所有的域。但是,你不能增加一些额外的域。  那么,什么时候增加域声明呢?

  • 当实体处理器所产生的域的名字,跟相应的域在schema.xml中的名字不一样的时候。

  • 当内嵌的转换器需要一些额外的信息来决定哪个域要处理,以及该怎么处理的时候。

  • XPathEntityprocessor 或者其他的处理器,显示的要求一些额外的信息的时候。

关于行(row)和多值域

行在DataimportHandler中的表现形式是一个Map。在这个map里面,key是域的名字,value可以任何一个合法的solr 类型。value也能够是合法的solr类型的聚集(这将会映射到一个多值域)。如果数据源是RDBMS的话,一般是不会产生多值域的。当然我们可以通过加一个子实体的方式来产生多值域。这里子实体返回的多个域,相当于父实体的一个多值域。如果数据源是xml的话,产生多值域是一件相当简单的事情。

变量

变量是指最终代替那些占位符的值。这是一个多级的map,每一个命名空间都是一个map,命名空间使用.分隔。例如 占位符 ${item.ID}, 'item'是一个命名空间(也是一个map),ID是这个命名空间下的一个值。我们很容易推导出 占位符 ${item.x.ID} 这里x是另外一个map。变量的值能够从Context中获得,也可以在RDMS的query属性中或者http数据源的url属性中使用类似${}的占位符获得。

使用函数来自定义query和url的格式

 命名空间这个概念在这里也是相当的有用的。用户可能想要传一个经过计算的值给 query或者url,比如这里有一个Data类型的数据,但是你的数据源只支持另外一种格式的数据源。我们提供了一些函数,或许它们能够帮你完成一些事情。

  • formatDate : 它可以像这样去使用,'${dataimporter.functions.formatDate(item.ID, yyyy-MM-dd HH:mm)}' 。它的第一个参数是一个合法的变量,第二个参数是一种时间格式(这里使用的格式工具是SimpledateFormat),The first argument can be a valid value from the VariableResolver and the second cvalue can be a a format string (use SimpledateFormat) . 它可以是一个经过计算的值,它使用solr的时间表示方式。(要注意,它必须被单引号括起来

  • escapeSql : 使用它可以对特别的sql 字符串进行包装。例子 : '${dataimporter.functions.escapeSql(item.ID)}'. 这里只使用一个参数,这个参数必须是一个合法的VaraiableResolver.

  • encodeUrl : 使用这个对url进行编码。例子e: '${dataimporter.functions.encodeUrl(item.ID)}' . 只使用一个参数,这个参数必须是一个合法的VariableResolver

访问请求参数

我们可以使用'request'命名空间来访问传递给http 请求的参数。例如'${dataimporter.request.command}' 将会返回被执行的命令。任何参数都可以通过这种方式得到。

交互式的开发模式Interactive Development Mode

这是一个很酷的,并且功能强大的工具。它能够帮助你通过图形界面来建立一个dataconfig.xml文档。你可以通过DataImport - 航梦 - 火星?地球? http://host:port/solr/admin/dataimport.jsp 来访问它。 以下是它的特性:

  • 这个界面有两个板块,RHS是用来获取输入的,LHS是用来显示输出的。

  • 当你点击debug now 按钮的时候,它将会执行配置文件,并且显示结果文档。

  • 你可以通过start和rows这两个参数来调试 类似从115开始到118这样的文档。

  • 选择 'verbose'选项表示你想要得到一些关于中间步骤的信息。包括query产生的数据,传给转换器的数据,以及转换器产生的数据。

  • 如果在运行过程中发生了异常,那么LHS板块将显示异常信息。

  • fields是由实体产生的。当域没有在schema.xml中声明,也没有在dataConfig.xml有声明的时候,转换器就不会对该域进行处理了。

屏幕快照

DataImport - 航梦 - 火星?地球?

哪里可以找到它?

DataimportHandler是solr的新加的特性。

  • 从 DataImport - 航梦 - 火星?地球? Solr website 下载一个最新的版本 。

  • 通过 Full Import 的例子来感受一下。

在Solr JIRA.的 DataImport - 航梦 - 火星?地球? SOLR-469 你可以查看到有关DataImporthandler的一些开发讨论。


 

第三部分:SOLR的db-data-config.xml高级进阶(处理CLOB和BLOB)

我们在使用solr处理数据库时,很多时候需要处理一下数据库中的CLOB、BLOB字段,对于这两种字段如何处理,下面以代码的形式给大家演示,(不用写Java代码啊)

1)定义数据源

    <dataSource name="ora" driver="oracle.jdbc.OracleDriver" url="...." />
    <datasource name="ds-BlobField" type="FieldStreamDataSource" />

 

2.)写一个blob字段处理

 <entity dataSource="ora" name="meta" query="select id, filename,content, bytes from documents" transformer="ClobTransformer">            
            <field column="ID" name="id" />
            <field column="FILENAME" name="filename" />
            <field column="CONTENT" name="CONTENT" clob="true" />

                <entity dataSource="ds-BlobField" processor="TikaEntityProcessor" url="FILE_CONTENT"
                    dataField="ATTACH.FILE_CONTENT">
                    <field column="text" name="FJ_FILE_CONTENT" /><!-- 全局搜索 -->
                    <field column="Author" name="FJ_FILE_AUTHOR" meta="true" />
                </entity>

 

 </entity>

这里简单介绍一下,上述蓝色字体是处理clob必须的,红色字体是处理blob必须的。

 

还是比较简单的吧。如果你还没看明白,,我也没辙了。哦,这里需要说明一下,使用上述代码需要依赖几个jar包:

tika-app-0.9.jar(巨大20M,不过非常好用,对于PDF、Excel、Word、PPT、RTF、TAR 、ZIP 等等吧,好多自己查吧。)

http://apache.etoak.com//pdfbox/1.6.0/pdfbox-app-1.6.0.jar

activation-1.1.jar

mail-1.4.1.jar

缺少了就跑不起来了,;)

posted @ 2012-11-15 19:55  浪浪仔  阅读(23351)  评论(0编辑  收藏  举报