在上篇中,我们通过在jetty中配置,是update需要进行用户名和密码认证,这篇中我们继续介绍如何在solrj中调用update

*测试添加文档
先尝试使用solrj,编写测试程序

1
2
3
4
String url = "http://localhost:8989/solr"; 
HttpSolrServer server = new HttpSolrServer(url);
SolrInputDocume doc1 = new SolrInputDocument();
server.add(docs);

提示401错误,添加用户名和密码:

1
2
3
4
5
String url = "http://localhost:8989/solr"; 
HttpSolrServer server = new HttpSolrServer(url);
HttpClientUtil.setBasicAuth((DefaultHttpClient) server.getHttpClient(), "index", "update");
SolrInputDocume doc1 = new SolrInputDocument();
server.add(docs);

提示 NonRepeatableRequestException, Cannot retry request with a non-repeatable request entity. 想跟踪过去,看看错误出自哪里,没办法调到源代码,于是尝试查询.
测试查询文档
将etc/webdefault.xml中对/update/
的限制改成,/select/*,编写查询代码,

1
2
3
4
5
String url = "http://localhost:8989/solr"; 
HttpSolrServer server = new HttpSolrServer(url);
SolrQuery query = new SolrQuery();
String q = "*:*";
query.setQuery(q);

提示401错误,添加用户名和密码:

1
2
3
4
5
String url = "http://localhost:8989/solr"; 
HttpSolrServer server = new HttpSolrServer(url);
SolrQuery query = new SolrQuery();
String q = "*:*";
query.setQuery(q);

查询成功,

*问题解决
不明白原因,只是猜测post的信息不能反复使用,在setBasicAuth前面有一段说明, “Currently this is not preemtive authentication. So it is not currently possible to do a post request while using this setting.”,意思就是认证过程不是最先进行的,所以现在不能用于post,可是认证过程可以用于get,于是察看get的执行过程,发现它先执行一次,发现要认证,于是再执行一次,而第二次执行时会先执行认证过程. 对于post过程,如果 可以执行同样的过程,那就可以达到目的,关键问题是”Cannot retry request with a non-repeatable request entity”,于是查看solr-4470是如何实现的,看到HttpSolrServer里代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if (contentStream[0] instanceof RequestWriter.LazyContentStream) {
post.setEntity(new InputStreamEntity(contentStream[0].getStream(), -1) {
@Override
public Header getContentType() {
return new BasicHeader("Content-Type", contentStream[0].getContentType());
}

@Override
public boolean isRepeatable() {
return false;
}

});
} else {
post.setEntity(new InputStreamEntity(contentStream[0].getStream(), -1) {
@Override
public Header getContentType() {
return new BasicHeader("Content-Type", contentStream[0].getContentType());
}

@Override
public boolean isRepeatable() {
return false;
}
});
}

修改成

1
2
3
4
5
6
7
8
9
10
11
HttpEntity entity = new InputStreamEntity(contentStream[0].getStream(), -1) {
@Override
public Header getContentType() {
return new BasicHeader("Content-Type", contentStream[0].getContentType());
}
@Override
public boolean isRepeatable() {
return false;
}
};
entity = new BufferedHttpEntity(entity);

在生产环境中,可以添加参数控制是否需要entity = new BufferedHttpEntity(entity);和HttpClientUtil.setBasicAuth((DefaultHttpClient) server.getHttpClient(), “index”, “update”);这两句

联系作者

有些情况下,想给Solr增加权限控制,这样就不会被随意更新和删除。关于这点,在https://wiki.apache.org/solr/SolrSecurity有详细的描述。觉得最坑人的一点是Solr-4470还没resolved。不管它,先使用Jetty添加权限控制

下载已经编译好的solr-4.8.0,进入example目录
编辑etc/webdefault.xml,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<security-constraint>
<web-resource-collection>
<web-resource-name>Solr authenticated application</web-resource-name>
<url-pattern>/update/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>update-role</role-name>
</auth-constraint>
</security-constraint>

<login-config>
<auth-method>BASIC</auth-method>
<realm-name>Solr Update</realm-name>
</login-config>

编辑 etc/jetty.xml, 添加如下内容:

1
2
3
4
5
6
7
8
9
<Call name="addBean">
<Arg>
<New class="org.eclipse.jetty.security.HashLoginService">
<Set name="name">Solr Update</Set>
<Set name="config"><SystemProperty name="jetty.home" default="."/>/etc/realm.properties</Set>
<Set name="refreshInterval">0</Set>
</New>
</Arg>
</Call>

增加 etc/realm.properties,写入如下内容,也就是用户名,密码以及角色:

1
index: update, update-role

启动solr,到exampledocs目录下执行./post.sh solr.xml,返回401错误,说明未认证。修改post.sh,在调用curl时加上用户名和密码,如下:
curl –user index:update $URL –data-binary @$f -H ‘Content-type:application/xml’

再次执行./post.sh solr.xml,执行成功,到solr后台查看,可以看到添加文件成功,说明认证设置成功

联系作者

从Lucene4.0开始,提供了扩展codec功能,这个功能主要是留给想自己定义索引格式的开发者。
在此之前,有必要了解codec主要的作用,codec相关的类主要作用是读写索引。 而通过实现FilterCodec,可以很方便的定义自己的codec。 这个方便主要是可以将许多读写索引部分交给已有的codec实现,而只实现自己需要改进的部分。当然如果这样还不能满足需求 可以重新写一个codec。

写个简单的例子更容易懂,
在Codec.java中,可以看到,读写索引主要实现以下几个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** Encodes/decodes postings */
public abstract PostingsFormat postingsFormat();

/** Encodes/decodes docvalues */
public abstract DocValuesFormat docValuesFormat();

/** Encodes/decodes stored fields */
public abstract StoredFieldsFormat storedFieldsFormat();

/** Encodes/decodes term vectors */
public abstract TermVectorsFormat termVectorsFormat();

/** Encodes/decodes field infos file */
public abstract FieldInfosFormat fieldInfosFormat();

/** Encodes/decodes segment info file */
public abstract SegmentInfoFormat segmentInfoFormat();

/** Encodes/decodes document normalization values */
public abstract NormsFormat normsFormat();

/** Encodes/decodes live docs */
public abstract LiveDocsFormat liveDocsFormat();

一个纯文本保存索引的codec是SimpleTextCodec,这个codec的主要目的是用来学习

下面定义自己的codec

1
2
3
4
5
6
7
8
9
10
public class HexinCodec extends FilterCodec {
final private FieldInfosFormat myTermFieldInfoFormat;
public HexinCodec() {
super("HexinCodec", new Lucene46Codec());
myTermFieldInfoFormat = new SimpleTextFieldInfosFormat();
}
public FieldInfosFormat fieldInfosFormat() {
return myTermFieldInfoFormat;
}
}

最后,还是让上面的例子跑起来,首先下载Lucene4.8.0的源码,之后在codecs/src/java下新建包org.apache.lucene.codecs.hexin,
在这个包下面新建类HexinCodec.java,复制上面的代码。
之后编写测试用的建索引程序Index.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package org.hexin;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.codecs.Codec;
import org.apache.lucene.codecs.hexin.HexinCodec;
import org.apache.lucene.codecs.lucene46.Lucene46Codec;
import org.apache.lucene.codecs.simpletext.SimpleTextCodec;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;

import java.io.File;
import java.io.IOException;
public class Index {
public static void main(String[] args) throws IOException {
//Codec codec = new SimpleTextCodec();
Codec codec = new HexinCodec();
//Codec codec = new Lucene46Codec();
String INDEX_DIR = "e:\\index";
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_48);
IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_48, analyzer);
iwc.setCodec(codec);
IndexWriter writer = null;
iwc.setOpenMode(OpenMode.CREATE);
iwc.setUseCompoundFile(false);
try {
writer = new IndexWriter(FSDirectory.open(new File(INDEX_DIR)), iwc);
Document doc = new Document();
doc.add(new TextField("title", "who are you, you are a man", Field.Store.YES));
doc.add(new TextField("content", "A long way to go there. Please drive a car", Field.Store.NO));
writer.addDocument(doc);
doc = new Document();
doc.add(new TextField("title", "are you sure", Field.Store.YES));
doc.add(new TextField("content", "He is a good man. He is a driver", Field.Store.NO));
writer.addDocument(doc);
writer.commit();
writer.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

编写测试用的搜索例子Search.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package org.hexin;
import java.io.File;
import java.util.Date;

import org.apache.lucene.codecs.Codec;
import org.apache.lucene.codecs.simpletext.SimpleTextCodec;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.FSDirectory;
public class Search {
private Search() {}
public static void main(String[] args) throws Exception {

String index = "e:\\index";
IndexReader reader = DirectoryReader.open(FSDirectory.open(new File(index)));
IndexSearcher searcher = new IndexSearcher(reader);
String queryString = "driver";
Query query = new TermQuery(new Term("content", queryString));
System.out.println("Searching for: " + query.toString());
Date start = new Date();
TopDocs results = searcher.search(query, null, 100);
Date end = new Date();
System.out.println("Time: "+(end.getTime()-start.getTime())+"ms");
ScoreDoc[] hits = results.scoreDocs;
int numTotalHits = results.totalHits;
System.out.println(numTotalHits + " total matching documents");
for (int i = 0; i < hits.length; i++) {
String output = "";
Document doc = searcher.doc(hits[i].doc);
output += "doc="+hits[i].doc+" score="+hits[i].score;
String title = doc.get("title");
if (title != null) {
output += " " + title;
}
System.out.println(output);
}
reader.close();
}
}

在Eclipse中运行Index.java,此时会报错
A SPI class of type org.apache.lucene.codecs.Codec with name ‘Lucene46’ does not exist. You need to add the corresponding
JAR file supporting this SPI to your classpath.The current classpath supports the following names:[]

问过定坤后,知道一个解决的办法是去官网下载已经编译过的Lucene二进制包,将其中的META-INF拷贝到core/src/java目录下,写上下面两行
org.apache.lucene.codecs.simpletext.SimpleTextCodec
org.apache.lucene.codecs.hexin.HexinCodec
此时即可运行通过。查看索引文件,有一个fld结尾的文件,其内容为文本文件,保存着字段值,这个文件就是通过SimpleTextCodec写入的,
而其它文件则是通过Lucene46Codec写入的。

联系作者

我们几乎每天都做这样的操作,输入账号和密码登陆跳转机,从跳转机输入帐号和密码登陆目标机器。当然输入账号和密码登陆跳转机可以在SecureCRT这些客户端中建立登录会话解决,可是后面这一步呢?事实上,后面这一步可以写一个脚本解决。

例如现在需要登录192.168.1.1这台机器,登录用户名和密码都为test,而要登录192.168.1.1,需要先登录到跳板机172.168.1.1,则我们可以新建会话链接192.168.1.1,在其中的会话选项中,ssh2中填上登录172.168.1.1需要的用户名和密码,在登录动作中,我们可以引用一个登录脚本。这里的登录动作指的是登录机器后需要进行的后续操作,在我们这里指的是登录跳板机后需要进行的操作,这当然是登录我们的目标主机了,于是可以写脚本,脚本的内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#$language = "VBScript"

#$interface = "1.0"
Sub main
' turn on synchronous mode so we don't miss any data
crt.Screen.Synchronous = True
crt.Screen.Send "ssh test@192.168.1.1" & VbCr
' Wait for a tring that looks like "password: " or "Password: "
crt.Screen.WaitForString "assword:"
' Send your password followed by a carriage return
crt.Screen.Send "test" & VbCr
' turn off synchronous mode to restore normal input processing
crt.Screen.Synchronous = False
End Sub

如此,我们只需要在SecureCRT中点击一下这个会话,就可以登录到192.168.1.1这台服务器了,是不是很方便?

联系作者

相信现在很多人还在用Solr1.4,因为Solr1.4许多时候还是满足需求了。可是总有一天会想升级,因为新版本中的一些功能和特性让使用Solr更加方便。而如果要从Solr1.4升级到Solr4.8,可以经过Solr1.4->Solr3.6->Solr4.0->Solr4.8这个步骤.

从Solr1.4->Solr3.6,去官网下载Solr3.6,使用需要升级的索引搭建起Solr引擎,执行curl ‘http://localhost:8983/solr/update?optimize=true&maxSegments=1&waitFlush=false‘ 即可

从Solr3.6->Solr4.0,去官网下载Solr4.0, 将lucene-core-4.0.jar拷贝到某一目录下,如:lib4.0/lucene-core-4.0.jar(注意,可能需要其它的包如:slf-api和log-back相关包,同样拷贝到lib4.0目录下), 之后执行java -cp “lib4.0/*” org.apache.lucene.index.IndexUpgrader -verbose index/, 这里 index目录存放着Solr3.6索引文件。

从Solr4.0->Solr4.8, 去官网下载Solr4.8,将lucene-core-4.8拷贝到某一目录下, 如:lib4.0/lucene-core-4.8.jar,之后执行../jdk1.7/bin/java -cp “lib4.8/*” org.apache.lucene.index.IndexUpgrader -delete-prior-commits -verbose index/,这里因为Solr4.8需要用到jdk1.7,所以执行java命令时,必须是jdk1.7。

联系作者

软连接和硬链接是Linux中经常用到的,详细介绍可以参考https://www.ibm.com/developerworks/cn/linux/l-cn-hardandsymb-links/

要知道软连接和硬链接的区别,必须知道了解Linux的文件系统设计,这其中就有inode这个概念。一个文件被分为用户数据和元数据,其中用户数据是数据存储的地方,而元数据中的inode则是指向这个地方,而文件名只是便于人们记忆而已。对于inode号,可以使用stat或者ls -i查看.

一个inode号可以对应多个文件名,这种情况下就是硬链接。因此创建硬链接并不需要拷贝用户数据,也就是不像cp命令那样,新创建一个inode号,所以创建硬链接速度非常快。只是硬链接有一个局限的地方就是只能对文件创建硬链接,并且不能跨越文件系统。需要注意的一个问题是,修改硬连接,原文件的内容也会修改。而修改原文件,也会修改硬连接。

而创建软连接则会创建新的inode号,只是这个inode号指向的用户数据很特殊,它指向创建软连接的文件。对于软连接,则没有硬链接的那些限制,它可以跨越文件系统,可以对目录创建软链接。只是当把原文件删除后,软连接就变成了死链接了。

联系作者

Solr关键概念

1.反向索引
2.检索词和布尔查询:
并查询:
+new +house 或者
new AND house
或查询:
new house 或者
new OR house
排除查询:
new house –rental 或者
new house NOT rental
短语查询:
“new home” OR “new house”
3 bedrooms” AND “walk in closet” AND “granite countertops”
分组查询:
New AND (house OR (home NOT improvement NOT depot NOT grown))
(+(buying purchasing -renting) +(home house residence –(+property -bedroom)))

对于短语查询,之所以可以实现,是因为在反向索引中保存了词在文档中的位置信息。

3.模糊查询
通配符查询:
如果需要查询以offic开头的词,只需要查询 offic
如果要使用通配符在开头的查询,如
ing,则需要将ReversedWildcardFilterFactory添加到字段分析链中

范围查询:
yearsOld:[18 TO 21] 18 <= x <= 21
yearsOld:{18 TO 21} 18 < x < 21
yearsOld:[18 TO 21} 18 <= x < 21
created:[2012-02-01T00:00.0Z TO 2012-08-02T00:00.0Z]

编辑距离查询:
administrator~ 默认编辑距离为1
administrator~1 编辑距离为1
administrator~2 编辑距离为2

临近查询:
“chief officer”~1 距离为1
例如: “chief executive officer”, “officer chief”

4.相关性:
Solr默认相关性,距离看文档

5.准确率和召回率
准确率说的是一次查询中,查询结果有多少是相关的比率
召回率说的是一次查询中,有多少相关结果被返回的比率

一般来说,搜索引擎都是尽量在二者中寻求一个平衡

6.Solr的一些局限
Solr无法执行想数据库查询那样复杂的查询
当更新一个跨越很多个文档的字段时,Solr将很麻烦
对于返回许多文档的查询,Solr的性能将会下降

联系作者

最近迷上了单元测试,在写单元测试时,提示一下错误:
java.lang.NoSuchMethodError: junit.framework.ComparisonFailure.getExpected()Ljava/lang/String;

莫名其妙的,assertFalse怎么可能没有。后来才知道,原来是版本冲突了,因为添加了好多个junit的jar本,而Eclipse只找到最低版本的,将一些低版本的jar去掉就好了。

添加jar这个问题真是蛋疼,在Eclipse里对引用的jar一个目录一个目录的添加,还要肉眼去把低版本的删除,真是麻烦。

联系作者

虽然在索引组,但有时还需要干解析的活,而这时,正则表达式就派上用场了。一段时间没写正则后,写起来就没有办法那么畅快,例如这次就是提取不到结果,想了之后,最后锁定在点号不能匹配换行符,试了之后,果然是这样。在Java中,加上Pattern.DOTALL就好了,以下就是用来提取雪球搜索页面里主要内容的函数,这个主要内容提取出来后是一个JSON格式的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static String getXueQiuContent(String httpBody) {
Pattern pattern = Pattern.compile("SNB.data.search\\s*?=\\s*?(\\{.+?\\});.*?seajs.use", Pattern.DOTALL);
Matcher m = pattern.matcher(httpBody);
if (m.find()) {
httpBody = m.group(1);
JSONObject obj;
try {
obj = new JSONObject(httpBody);
JSONArray jsonArr = (JSONArray) obj.get("list");
httpBody = jsonArr.toString();
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
return httpBody;
}

如果不知道这个Pattern.DOTALL,其实用[\s\S]也是可以得,因为\s匹配空白字符,\S匹配非空白字符,两者合在一起就可以匹配任何字符了。
对于爬虫组来说,要发现新的站点,都雪球这些网站去搜索一番还是可以尝试的。

联系作者

最近因为负责一个小功能,所以想尽力做好它。于是对会经常看看用户的查询,看看这些查询的结果是否满足需要,于是需要对这些查询词进行提取。本来还想用Python来写的,后来想想shell才是做这事的最佳方法,于是先从grep开始。

solr的日志中,query都是跟在‘q=’后面,且参数间用&隔开,于是执行如下命令,
grep -o ‘q=.*\&’ solr.log
得到如下结果
q=磐安&macro.skip=0&qt=macro&wt=json&
q=磐安+财政&macro.skip=0&qt=macro&wt=json&
q=保定+财政&macro.skip=0&qt=macro&wt=json&
q=磐安+财政&macro.skip=0&qt=macro&wt=json&
q=财政+长春&macro.skip=0&qt=macro&wt=json&
q=财政+长沙&macro.skip=0&qt=macro&wt=json&
q=存款收入&macro.skip=0&qt=macro&wt=json&
q=存款收入&qt=macro&wt=json&macro.groupOffset=0&macro.groupNames=利率走势&
q=存款收入&qt=macro&wt=json&macro.groupOffset=0&macro.groupNames=行业经济&
q=存款收入&qt=macro&wt=json&macro.groupOffset=0&macro.groupNames=区域宏观&
q=存款收入&qt=macro&wt=json&macro.groupOffset=0&macro.groupNames=中国宏观&

之后就是截取query部分,这时awk就派上用场了。先用&分割,得到第一段,之后用=分割,得到第二段
grep -o ‘q=.*\&’ solr.log | grep -v ‘module2:’ | grep -v ‘solrconfig.xml’ | awk -F ‘&’ ‘{print $1}’ | awk -F ‘=’ ‘{print $2}’
结果如下:
磐安
磐安+财政
保定+财政
磐安+财政
财政+长春
财政+长沙
存款收入
存款收入
存款收入
存款收入
存款收入

之后想统计每个查询词的次数,此时先用sort排序,之后用uniq -c来统计,
grep -o ‘q=.*\&’ solr.log | grep -v ‘module2:’ | grep -v ‘solrconfig.xml’ | awk -F ‘&’ ‘{print $1}’ | awk -F ‘=’ ‘{print $2}’ |sort | uniq -c
结果如下:
1 保定+财政
5 存款收入
1 磐安
2 磐安+财政
1 财政+长春
1 财政+长沙

而我希望按查询次数从高到低排列,于是再用sort -rn
grep -o ‘q=.*\&’ solr.log | grep -v ‘module2:’ | grep -v ‘solrconfig.xml’ | awk -F ‘&’ ‘{print $1}’ | awk -F ‘=’ ‘{print $2}’ |sort | uniq -c | sort -rn
结果如下:
5 存款收入
2 磐安+财政
1 财政+长沙
1 财政+长春
1 磐安
1 保定+财政

一行代码搞定。一句话,管道实在是太方便了,linux也是如此。

联系作者