基于JieBaNet+Lucene.Net实现全文搜索

 

实现效果:

  上一篇文章有附全文搜索结果的设计图,下面截一张开发完成上线后的实图:

  基本风格是模仿的百度搜索结果,绿色的分页略显小清新。

  目前已采集并创建索引的文章约3W多篇,索引文件不算太大,查询速度非常棒。

  

刀不磨要生锈,人不学要落后。每天都要学一些新东西。

基本技术介绍:

  还记得上一次做全文搜索是在2013年,主要核心设计与代码均是当时的架构师写的,自己只能算是全程参与。

  当时使用的是经典搭配:盘古分词+Lucene.net。

  前几篇文章有说到,盘古分词已经很多年不更新了,我在SupportYun系统一直引用的JieBaNet来做分词技术。

  那么是否也有成型的JieBaNet+Lucene.Net的全文搜索方案呢?

  经过多番寻找,在GitHub上面找到一个简易的例子:https://github.com/anderscui/jiebaForLuceneNet

  博主下面要讲的实现方案就是从这个demo得到的启发,大家有兴趣可以去看看这个demo。

  博主使用的具体版本:Lucene.net 3.0.3.0 ,JieBaNet 0.38.3.0(做过简易的调整与扩展,前面文章有讲到)

  首先我们对Lucene.Net的分词器Tokenizer、分析器Analyzer做一个基于JieBaNet的扩展。

  1.基于LuceneNet扩展的JieBa分析器JiebaForLuceneAnalyzer  

 1     /// <summary>
2 /// 基于LuceneNet扩展的JieBa分析器
3 /// </summary>
4 public class JiebaForLuceneAnalyzer : Analyzer
5 {
6 protected static readonly ISet<string> DefaultStopWords = StopAnalyzer.ENGLISH_STOP_WORDS_SET;
7
8 private static ISet<string> StopWords;
9
10 static JiebaForLuceneAnalyzer()
11 {
12 StopWords = new HashSet<string>();
13 var stopWordsFile = Path.GetFullPath(JiebaNet.Analyser.ConfigManager.StopWordsFile);
14 if (File.Exists(stopWordsFile))
15 {
16 var lines = File.ReadAllLines(stopWordsFile);
17 foreach (var line in lines)
18 {
19 StopWords.Add(line.Trim());
20 }
21 }
22 else
23 {
24 StopWords = DefaultStopWords;
25 }
26 }
27
28 public override TokenStream TokenStream(string fieldName, TextReader reader)
29 {
30 var seg = new JiebaSegmenter();
31 TokenStream result = new JiebaForLuceneTokenizer(seg, reader);
32 result = new LowerCaseFilter(result);
33 result = new StopFilter(true, result, StopWords);
34 return result;
35 }
36 }

  2.基于LuceneNet扩展的JieBa分词器:JiebaForLuceneTokenizer

 1     /// <summary>
2 /// 基于Lucene的JieBa分词扩展
3 /// </summary>
4 public class JiebaForLuceneTokenizer:Tokenizer
5 {
6 private readonly JiebaSegmenter segmenter;
7 private readonly ITermAttribute termAtt;
8 private readonly IOffsetAttribute offsetAtt;
9 private readonly ITypeAttribute typeAtt;
10
11 private readonly List<Token> tokens;
12 private int position = -1;
13
14 public JiebaForLuceneTokenizer(JiebaSegmenter seg, TextReader input):this(seg, input.ReadToEnd()) { }
15
16 public JiebaForLuceneTokenizer(JiebaSegmenter seg, string input)
17 {
18 segmenter = seg;
19 termAtt = AddAttribute<ITermAttribute>();
20 offsetAtt = AddAttribute<IOffsetAttribute>();
21 typeAtt = AddAttribute<ITypeAttribute>();
22
23 var text = input;
24 tokens = segmenter.Tokenize(text, TokenizerMode.Search).ToList();
25 }
26
27 public override bool IncrementToken()
28 {
29 ClearAttributes();
30 position++;
31 if (position < tokens.Count)
32 {
33 var token = tokens[position];
34 termAtt.SetTermBuffer(token.Word);
35 offsetAtt.SetOffset(token.StartIndex, token.EndIndex);
36 typeAtt.Type = "Jieba";
37 return true;
38 }
39
40 End();
41 return false;
42 }
43
44 public IEnumerable<Token> Tokenize(string text, TokenizerMode mode = TokenizerMode.Search)
45 {
46 return segmenter.Tokenize(text, mode);
47 }
48 }

理想如果不向现实做一点点屈服,那么理想也将归于尘土。

实现方案设计:

  我们做全文搜索的设计时一定会考虑的一个问题就是:我们系统是分很多模块的,不同模块的字段差异很大,怎么才能实现同一个索引,既可以单个模块搜索又可以全站搜索,甚至按一些字段做条件来搜索呢?

  这些也是SupportYun系统需要考虑的问题,因为目前的数据就天然的拆分成了活动、文章两个类别,字段也大有不同。博主想实现的是一个可以全站搜索(结果包括活动、文章),也可以在文章栏目/活动栏目分别搜索,并且可以按几个指定字段来做搜索条件。

  要做一个这样的全文搜索功能,我们需要从程序设计上来下功夫。下面就介绍一下博主的设计方案:

  一、索引创建

    

    1.我们设计一个IndexManager来处理最基本的索引创建、更新、删除操作。

 

    2.创建、更新使用到的标准数据类:IndexContent。

    我们设计TableName(对应DB表名)、RowId(对应DB主键)、CollectTime(对应DB数据创建时间)、ModuleType(所属系统模块)、Title(检索标题)、IndexTextContent(检索文本)等六个基础字段,所有模块需要创建索引必须构建该6个字段(大家可据具体情况扩展)。

    然后设计10个预留字段Tag1-Tag10,用以兼容各大模块其他不同字段。

    预留字段的存储、索引方式可独立配置。

 

    其中BaseIndexContent含有六个基础字段。

    3.创建一个子模块索引构建器的接口:IIndexBuilder。

    各子模块通过继承实现IIndexBuilder,来实现索引的操作。

 

    4.下面我们以活动模块为例,来实现索引创建。

    a)首先创建一个基于活动模块的数据类:ActivityIndexContent,可以将我们需要索引或存储的字段都设计在内。

 

    b)我们再创建ActivityIndexBuilder并继承IIndexBuilder,实现其创建、更新、删除方法。

 

    代码就不解释了,很简单。主要就是调用IndexManager来执行操作。

    我们只需要在需要创建活动数据索引的业务点,构建ActivityIndexBuilder对象,并构建ActivityIndexContent集合作为参数,调用BuildIndex方法即可。

  二、全文搜索

    全文搜索我们采用同样的设计方式。

    1.设计一个抽象的搜索类:BaseIndexSearch,所有搜索模块(包括全站)均需继承它来实现搜索效果。

  1     public abstract class BaseIndexSearch<TIndexSearchResultItem>
2 where TIndexSearchResultItem : IndexSearchResultItem
3 {
4 /// <summary>
5 /// 索引存储目录
6 /// </summary>
7 private static readonly string IndexStorePath = ConfigurationManager.AppSettings["IndexStorePath"];
8 private readonly string[] fieldsToSearch;
9 protected static readonly SimpleHTMLFormatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
10 private static IndexSearcher indexSearcher = null;
11
12 /// <summary>
13 /// 索引内容命中片段大小
14 /// </summary>
15 public int FragmentSize { get; set; }
16
17 /// <summary>
18 /// 构造方法
19 /// </summary>
20 /// <param name="fieldsToSearch">搜索文本字段</param>
21 protected BaseIndexSearch(string[] fieldsToSearch)
22 {
23 FragmentSize = 100;
24 this.fieldsToSearch = fieldsToSearch;
25 }
26
27 /// <summary>
28 /// 创建搜索结果实例
29 /// </summary>
30 /// <returns></returns>
31 protected abstract TIndexSearchResultItem CreateIndexSearchResultItem();
32
33 /// <summary>
34 /// 修改搜索结果(主要修改tag字段对应的属性)
35 /// </summary>
36 /// <param name="indexSearchResultItem">搜索结果项实例</param>
37 /// <param name="content">用户搜索内容</param>
38 /// <param name="docIndex">索引库位置</param>
39 /// <param name="doc">当前位置内容</param>
40 /// <returns>搜索结果</returns>
41 protected abstract void ModifyIndexSearchResultItem(ref TIndexSearchResultItem indexSearchResultItem, string content, int docIndex, Document doc);
42
43 /// <summary>
44 /// 修改筛选器(各模块)
45 /// </summary>
46 /// <param name="filter"></param>
47 protected abstract void ModifySearchFilter(ref Dictionary<string, string> filter);
48
49 /// <summary>
50 /// 全库搜索
51 /// </summary>
52 /// <param name="content">搜索文本内容</param>
53 /// <param name="filter">查询内容限制条件,默认为null,不限制条件.</param>
54 /// <param name="fieldSorts">对字段进行排序</param>
55 /// <param name="pageIndex">查询结果当前页,默认为1</param>
56 /// <param name="pageSize">查询结果每页结果数,默认为20</param>
57 public PagedIndexSearchResult<TIndexSearchResultItem> Search(string content
58 , Dictionary<string, string> filter = null, List<FieldSort> fieldSorts = null
59 , int pageIndex = 1, int pageSize = 20)
60 {
61 try
62 {
63 if (!string.IsNullOrEmpty(content))
64 {
65 content = ReplaceIndexSensitiveWords(content);
66 content = GetKeywordsSplitBySpace(content,
67 new JiebaForLuceneTokenizer(new JiebaSegmenter(), content));
68 }
69 if (string.IsNullOrEmpty(content) || pageIndex < 1)
70 {
71 throw new Exception("输入参数不符合要求(用户输入为空,页码小于等于1)");
72 }
73
74 var stopWatch = new Stopwatch();
75 stopWatch.Start();
76
77 Analyzer analyzer = new JiebaForLuceneAnalyzer();
78 // 索引条件创建
79 var query = MakeSearchQuery(content, analyzer);
80 // 筛选条件构建
81 filter = filter == null ? new Dictionary<string, string>() : new Dictionary<string, string>(filter);
82 ModifySearchFilter(ref filter);
83 Filter luceneFilter = MakeSearchFilter(filter);
84
85 #region------------------------------执行查询---------------------------------------
86
87 TopDocs topDocs;
88 if (indexSearcher == null)
89 {
90 var dir = new DirectoryInfo(IndexStorePath);
91 FSDirectory entityDirectory = FSDirectory.Open(dir);
92 IndexReader reader = IndexReader.Open(entityDirectory, true);
93 indexSearcher = new IndexSearcher(reader);
94 }
95 else
96 {
97 IndexReader indexReader = indexSearcher.IndexReader;
98 if (!indexReader.IsCurrent())
99 {
100 indexSearcher.Dispose();
101 indexSearcher = new IndexSearcher(indexReader.Reopen());
102 }
103 }
104 // 收集器容量为所有
105 int totalCollectCount = pageIndex*pageSize;
106 Sort sort = GetSortByFieldSorts(fieldSorts);
107 topDocs = indexSearcher.Search(query, luceneFilter, totalCollectCount, sort ?? Sort.RELEVANCE);
108
109 #endregion
110
111 #region-----------------------返回结果生成-------------------------------
112
113 ScoreDoc[] hits = topDocs.ScoreDocs;
114 var start = (pageIndex - 1)*pageSize + 1;
115 var end = Math.Min(totalCollectCount, hits.Count());
116
117 var result = new PagedIndexSearchResult<TIndexSearchResultItem>
118 {
119 PageIndex = pageIndex,
120 PageSize = pageSize,
121 TotalRecords = topDocs.TotalHits
122 };
123
124 for (var i = start; i <= end; i++)
125 {
126 var scoreDoc = hits[i - 1];
127 var doc = indexSearcher.Doc(scoreDoc.Doc);
128
129 var indexSearchResultItem = CreateIndexSearchResultItem();
130 indexSearchResultItem.DocIndex = scoreDoc.Doc;
131 indexSearchResultItem.ModuleType = doc.Get("ModuleType");
132 indexSearchResultItem.TableName = doc.Get("TableName");
133 indexSearchResultItem.RowId = Guid.Parse(doc.Get("RowId"));
134 if (!string.IsNullOrEmpty(doc.Get("CollectTime")))
135 {
136 indexSearchResultItem.CollectTime = DateTime.Parse(doc.Get("CollectTime"));
137 }
138 var title = GetHighlighter(formatter, FragmentSize).GetBestFragment(content, doc.Get("Title"));
139 indexSearchResultItem.Title = string.IsNullOrEmpty(title) ? doc.Get("Title") : title;
140 var text = GetHighlighter(formatter, FragmentSize)
141 .GetBestFragment(content, doc.Get("IndexTextContent"));
142 indexSearchResultItem.Content = string.IsNullOrEmpty(text)
143 ? (doc.Get("IndexTextContent").Length > 100
144 ? doc.Get("IndexTextContent").Substring(0, 100)
145 : doc.Get("IndexTextContent"))
146 : text;
147 ModifyIndexSearchResultItem(ref indexSearchResultItem, content, scoreDoc.Doc, doc);
148 result.Add(indexSearchResultItem);
149 }
150 stopWatch.Stop();
151 result.Elapsed = stopWatch.ElapsedMilliseconds*1.0/1000;
152
153 return result;
154
155 #endregion
156 }
157 catch (Exception exception)
158 {
159 LogUtils.ErrorLog(exception);
160 return null;
161 }
162 }
163
164 private Sort GetSortByFieldSorts(List<FieldSort> fieldSorts)
165 {
166 if (fieldSorts == null)
167 {
168 return null;
169 }
170 return new Sort(fieldSorts.Select(fieldSort => new SortField(fieldSort.FieldName, SortField.FLOAT, !fieldSort.Ascend)).ToArray());
171 }
172
173 private static Filter MakeSearchFilter(Dictionary<string, string> filter)
174 {
175 Filter luceneFilter = null;
176 if (filter != null && filter.Keys.Any())
177 {
178 var booleanQuery = new BooleanQuery();
179 foreach (KeyValuePair<string, string> keyValuePair in filter)
180 {
181 var termQuery = new TermQuery(new Term(keyValuePair.Key, keyValuePair.Value));
182 booleanQuery.Add(termQuery, Occur.MUST);
183 }
184 luceneFilter = new QueryWrapperFilter(booleanQuery);
185 }
186 return luceneFilter;
187 }
188
189 private Query MakeSearchQuery(string content, Analyzer analyzer)
190 {
191 var query = new BooleanQuery();
192 // 总查询参数
193 // 属性查询
194 if (!string.IsNullOrEmpty(content))
195 {
196 QueryParser parser = new MultiFieldQueryParser(Version.LUCENE_30, fieldsToSearch, analyzer);
197 Query queryObj;
198 try
199 {
200 queryObj = parser.Parse(content);
201 }
202 catch (ParseException parseException)
203 {
204 throw new Exception("在FileLibraryIndexSearch中构造Query时出错。", parseException);
205 }
206 query.Add(queryObj, Occur.MUST);
207 }
208 return query;
209 }
210
211 private string GetKeywordsSplitBySpace(string keywords, JiebaForLuceneTokenizer jiebaForLuceneTokenizer)
212 {
213 var result = new StringBuilder();
214
215 var words = jiebaForLuceneTokenizer.Tokenize(keywords);
216
217 foreach (var word in words)
218 {
219 if (string.IsNullOrWhiteSpace(word.Word))
220 {
221 continue;
222 }
223
224 result.AppendFormat("{0} ", word.Word);
225 }
226
227 return result.ToString().Trim();
228 }
229
230 private string ReplaceIndexSensitiveWords(string str)
231 {
232 str = str.Replace("+", "");
233 str = str.Replace("+", "");
234 str = str.Replace("-", "");
235 str = str.Replace("-", "");
236 str = str.Replace("!", "");
237 str = str.Replace("!", "");
238 str = str.Replace("(", "");
239 str = str.Replace(")", "");
240 str = str.Replace("(", "");
241 str = str.Replace(")", "");
242 str = str.Replace(":", "");
243 str = str.Replace(":", "");
244 str = str.Replace("^", "");
245 str = str.Replace("[", "");
246 str = str.Replace("]", "");
247 str = str.Replace("【", "");
248 str = str.Replace("】", "");
249 str = str.Replace("{", "");
250 str = str.Replace("}", "");
251 str = str.Replace("{", "");
252 str = str.Replace("}", "");
253 str = str.Replace("~", "");
254 str = str.Replace("~", "");
255 str = str.Replace("*", "");
256 str = str.Replace("*", "");
257 str = str.Replace("?", "");
258 str = str.Replace("?", "");
259 return str;
260 }
261
262 protected Highlighter GetHighlighter(Formatter formatter, int fragmentSize)
263 {
264 var highlighter = new Highlighter(formatter, new Segment()) { FragmentSize = fragmentSize };
265 return highlighter;
266 }
267 }

    几个protected abstract方法,是需要继承的子类来实现的。

    其中为了实现搜索结果对命中关键词进行高亮显示,特引用了盘古分词的Highlighter。原则是此处应该是参照盘古分词的源码,自己使用JieBaNet来做实现的,由于工期较紧,直接引用了盘古。

    2.我们设计一个IndexSearchResultItem,表示搜索结果的基类。

 

    3.我们来看看具体的实现,先来看全站搜索的SearchService

 1     public class IndexSearch : BaseIndexSearch<IndexSearchResultItem>
2 {
3 public IndexSearch()
4 : base(new[] { "IndexTextContent", "Title" })
5 {
6 }
7
8 protected override IndexSearchResultItem CreateIndexSearchResultItem()
9 {
10 return new IndexSearchResultItem();
11 }
12
13 protected override void ModifyIndexSearchResultItem(ref IndexSearchResultItem indexSearchResultItem, string content,
14 int docIndex, Document doc)
15 {
16 //不做修改
17 }
18
19 protected override void ModifySearchFilter(ref Dictionary<string, string> filter)
20 {
21 //不做筛选条件修改
22 }
23 }

    是不是非常简单。由于我们此处搜索的是全站,结果展示直接用基类,取出基本字段即可。

    4.再列举一个活动的搜索实现。

    a)我们首先创建一个活动搜索结果类ActivityIndexSearchResultItem,继承自结果基类IndexSearchResultItem

 

    b)然后创建活动模块的搜索服务:ActivityIndexSearch,同样需要继承BaseIndexSearch,这时候ActivityIndexSearch只需要相对全站搜索修改几个参数即可。

 1     public class ActivityIndexSearch: BaseIndexSearch<ActivityIndexSearchResultItem>
2 {
3 public ActivityIndexSearch()
4 : base(new[] { "IndexTextContent", "Title" })
5 {
6 }
7
8 protected override ActivityIndexSearchResultItem CreateIndexSearchResultItem()
9 {
10 return new ActivityIndexSearchResultItem();
11 }
12
13 protected override void ModifyIndexSearchResultItem(ref ActivityIndexSearchResultItem indexSearchResultItem, string content,
14 int docIndex, Document doc)
15 {
16 indexSearchResultItem.ActivityTypes = doc.Get("Tag1");
17 indexSearchResultItem.Url = doc.Get("Tag2");
18 indexSearchResultItem.SourceName = doc.Get("Tag3");
19 indexSearchResultItem.SourceOfficialHotline = doc.Get("Tag4");
20 indexSearchResultItem.SourceUrl = doc.Get("Tag5");
21 indexSearchResultItem.CityId=new Guid(doc.Get("Tag6"));
22 indexSearchResultItem.Address = doc.Get("Tag7");
23 indexSearchResultItem.ActivityDate = doc.Get("Tag8");
24 }
25
26 protected override void ModifySearchFilter(ref Dictionary<string, string> filter)
27 {
28 filter.Add("ModuleType", "活动");
29 }
30 }

    筛选条件加上模块=活动,返回结果数据类指定,活动特有字段返回赋值。

    业务调用就非常简单了。

    全站全文搜索:我们直接new IndexSearch(),然后调用其Search()方法

    活动全文搜索:我们直接new ActivityIndexSearch(),然后调用其Search()方法

    Search()方法几个参数:

    ///<param name="content">搜索文本内容</param>
    /// <param name="filter">查询内容限制条件,默认为null,不限制条件.</param>
    /// <param name="fieldSorts">对字段进行排序</param>
    /// <param name="pageIndex">查询结果当前页,默认为1</param>
    /// <param name="pageSize">查询结果每页结果数,默认为20</param>

如果我们用软能力而不是用技术能力来区分程序员的好坏 – 是不是有那么点反常和变态。

JieBaNet+Lucene.Net的更多相关文章

  1. 记一次企业级爬虫系统升级改造(五):基于JieBaNet+Lucene.Net实现全文搜索

    实现效果: 上一篇文章有附全文搜索结果的设计图,下面截一张开发完成上线后的实图: 基本风格是模仿的百度搜索结果,绿色的分页略显小清新. 目前已采集并创建索引的文章约3W多篇,索引文件不算太大,查询速度 ...

  2. 基于JieBaNet+Lucene.Net实现全文搜索

    实现效果: 上一篇文章有附全文搜索结果的设计图,下面截一张开发完成上线后的实图: 基本风格是模仿的百度搜索结果,绿色的分页略显小清新. 目前已采集并创建索引的文章约3W多篇,索引文件不算太大,查询速度 ...

  3. Lucene.net(4.8.0) 学习问题记录五: JIEba分词和Lucene的结合,以及对分词器的思考

    前言:目前自己在做使用Lucene.net和PanGu分词实现全文检索的工作,不过自己是把别人做好的项目进行迁移.因为项目整体要迁移到ASP.NET Core 2.0版本,而Lucene使用的版本是3 ...

  4. 【netcore基础】.Net core通过 Lucene.Net 和 jieba.NET 处理分词搜索功能

    业务要求是对商品标题可以进行模糊搜索 例如用户输入了[我想查询下雅思托福考试],这里我们需要先将这句话分词成[查询][雅思][托福][考试],然后搜索包含相关词汇的商品. 思路如下 首先我们需要把数据 ...

  5. ]NET Core Lucene.net和PanGu分词实现全文检索

    Lucene.net和PanGu分词实现全文检索 Lucene.net(4.8.0) 学习问题记录五: JIEba分词和Lucene的结合,以及对分词器的思考   前言:目前自己在做使用Lucene. ...

  6. lucene 基础知识点

    部分知识点的梳理,参考<lucene实战>及网络资料 1.基本概念 lucence 可以认为分为两大组件: 1)索引组件 a.内容获取:即将原始的内容材料,可以是数据库.网站(爬虫).文本 ...

  7. 用lucene替代mysql读库的尝试

    采用lucene对mysql中的表建索引,并替代全文检索操作. 备注:代码临时梳理很粗糙,后续修改. import java.io.File; import java.io.IOException; ...

  8. Lucene的评分(score)机制研究

    首先,需要学习Lucene的评分计算公式—— 分值计算方式为查询语句q中每个项t与文档d的匹配分值之和,当然还有权重的因素.其中每一项的意思如下表所示: 表3.5 评分公式中的因子 评分因子 描 述 ...

  9. Lucene的分析资料【转】

    Lucene 源码剖析 1 目录 2 Lucene是什么 2.1.1 强大特性 2.1.2 API组成- 2.1.3 Hello World! 2.1.4 Lucene roadmap 3 索引文件结 ...

随机推荐

  1. UVA 11090 - Going in Cycle!! SPFA

    http://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&p ...

  2. keil出错总结

    错误一: ..\APP\app.c(51): error:  #268: declaration may not appear after executable statement in block ...

  3. spark安装与调试

    I---- 1---jdk and scala install ****zyp@ubuntu:~/Desktop/software$ tar xvf jdk-7u67-linux-i586.tar.g ...

  4. Maven项目中mvn clean后找不到測试类问题

    在Maven项目中进行单元測试,但mvn clean后又一次mvn install项目,再次进行单元測试.会有下面的错误. <span style="font-family:KaiTi ...

  5. swift学习第九天:可选类型以及应用场景

    可选类型的介绍 注意: 可选类型时swift中较理解的一个知识点 暂时先了解,多利用Xcode的提示来使用 随着学习的深入,慢慢理解其中的原理和好处 概念: 在OC开发中,如果一个变量暂停不使用,可以 ...

  6. Spring boot(一) 入门

    本系列基于Eclipse 4.7 .JDK 8 一.下载STS (1)STS 注意自己的eclipse版本. 在 Update Site Archives 里面选择对应eclipse的版本下载. (2 ...

  7. Jupyter Notebook 常用快捷键

    Jupyter Notebook 提供了比 IPython 美观的多得多的可视化形式.(比如对于 pandas 下的 DataFrame 的展示,df.head(5)) Jupyter Noteboo ...

  8. Linux基本命令(一)

    目标 熟练使用 Linux常用的命令 ls clear cd pwd mkdir touch rm cp mv tree chmod find grep 重定向 软连接.硬链接 压缩 shutdown ...

  9. [Node.js] Initialize a LoopBack Node.js Project through the CLI

    LoopBack is a framework built on top of Express for creating APIs. It allows you to create end-to-en ...

  10. mysqlbinlog命令使用

    常用参数:--start-datetime=datetime 从二进制日志中第1个日期时间等于或晚于datetime参量的事件开始读取.datetime值相对于运行mysqlbinlog的机器上的本地 ...