wxpython怎么用_使用wxPython打造印象笔记(18)笔记搜索功能

在前面的文章中我们已经实现了笔记和笔记本的创建、编辑、删除功能,今天我们来实现笔记搜索功能:当用户在搜索框中输入文字时,笔记列表会实时的显示搜索结果,匹配规则为笔记的标题或者正文中包含关键字(词)

如果笔记的正文中包含关键词,那么选中该笔记时将高亮关键词

支持全局搜索,能够从所有的笔记本中搜索关键词

最终效果如下:v2-2cbe60380d6d6dd3abd6010c77047354.jpg笔记搜索功能演示https://www.zhihu.com/video/1241794511703580672

使用Whoosh实现全文搜索

Whoosh是一个使用Python实现的全文搜索引擎库,它使用简便、快速可靠,详细介绍参考Introduction to Whoosh​whoosh.readthedocs.io

首先安装Whoosh:

pip install Whoosh

在根目录下新建一个文件夹 services,然后新建 note_search_service.py 文件。

创建索引对象

在创建索引之前,需要指定索引目录,编辑 config.ini 文件,添加 index_dir:

[app]

data_dir = data

note_dir = notes

index_dir = data/indexes

# ....

编辑 note_search_service.py 文件,在初始化时如果索引不存在,则根据模式创建索引,否则打开索引对象。

import whoosh.index

from config import CONFIG

from whoosh.fields import NUMERIC, TEXT, Schema

from jieba.analyse import ChineseAnalyzer

class NoteSearchService:

def __init__(self):

if whoosh.index.exists_in(CONFIG['app']['index_dir']):

self.index = whoosh.index.open_dir(CONFIG['app']['index_dir'])

else:

schema = Schema(note_id=NUMERIC(stored=True, unique=True),

notebook_id=NUMERIC(stored=True),

title=TEXT(analyzer=ChineseAnalyzer()),

snippet=TEXT(analyzer=ChineseAnalyzer()))

self.index = whoosh.index.create_in(CONFIG['app']['index_dir'], schema)

这里创建了一个模式对象,里面有如下字段:note_id为数字类型的字段,stored=True 表示此字段会储存在索引文件中,unique=True 表示唯一性,这样就可以根据 note_id 来删除或者更新索引

notebook_id 和 note_id 类似,只是不具备唯一性

title 为文本类型的字段,也就是笔记标题索引。注意 analyzer 后面使用了结巴分词提供的中文分词器(whoosh 默认不支持中文分词)

snippet 为笔记纯文本,和 title 字段类似

初始化之后,我们就可以拿到索引对象。

上面提到了结巴分词,我们需要安装一下:

pip install jieba

添加索引内容

如果我们新建了一条笔记,则需要给这条笔记添加索引。

def add_doc(self, note):

with self.index.writer() as writer:

writer.add_document(**self._get_fields(note))

@staticmethod

def _get_fields(note):

return {'note_id': note.id, 'notebook_id': note.notebook_id, 'title': note.title, 'snippet': note.snippet}

索引对象的writer方法会返回一个用来操作索引文件的对象,调用 add_document 方法即可添加索引内容。

因为后面的一些方法也会进行类似的操作,所以定义了一个 _get_fields 方法来简化代码。

删除索引内容

如果笔记删除了,那么它对应的索引内容也需要删除。

def delete_doc(self, note):

self.index.delete_by_term('note_id', note.id)

因为在前面的模式对象中,note_id 字段设置成了唯一性,所以就可以根据 note_id 来删除对应的索引记录。

更新索引内容

如果笔记标题或者正文内容有所变化,其对应的索引内容也需要作相应的更新。

def update_doc(self, note):

with self.index.writer() as writer:

writer.update_document(**self._get_fields(note))

查找索引内容

如果笔记标题或者正文包含关键词,并且满足笔记本约束的话(属于当前笔记本或者全局范围内),说明此笔记成功匹配关键词。

# ...

from whoosh.qparser import MultifieldParser

from whoosh import query

# ...

def search(self, keyword, notebook_id=None):

with self.index.searcher() as searcher:

query_parser = MultifieldParser(["title", "snippet"], schema=self.index.schema).parse(keyword)

notebook_filter = query.Term("notebook_id", notebook_id) if notebook_id else None

results = searcher.search(query_parser, filter=notebook_filter, limit=None)

return [res['note_id'] for res in results]

Whoosh提供了一个多字段查询解析器 MultifieldParser,用在这里非常合适。

因为支持全局搜索或当前笔记本内搜索,所以我们需要配置过滤器参数,当 notebook_id 为空时表示全局搜索,否则在当前笔记本中搜索。

同步更新索引

当笔记增删改时,其索引内容也需要更新。编辑 views / list_panel.py 文件:

from services.note_search_service import NoteSearchService

def __init__(self, parent):

# ...

self.searcher = NoteSearchService()

pub.subscribe(self._on_note_updated, 'note.updated')

# ...

def _on_note_created(self, note):

# ...

self.searcher.add_doc(note)

def _on_note_updated(self, note):

self.searcher.update_doc(note)

def _on_note_deleting(self, note):

self.searcher.delete_doc(note)

# ...

实现搜索逻辑

编辑 views / header_panel.py:

def __init__(self, parent):

# ...

self._global_search_menu_id = wx.NewIdRef()

self.is_global_search = False

def _init_event(self):

# ...

self.search_bar.Bind(wx.EVT_TEXT, self._on_searching)

self.Bind(wx.EVT_MENU, self._on_global_search_menu_checked, id=self._global_search_menu_id)

def _on_searching(self, e):

pub.sendMessage('note.searching', keyword=self.keyword, is_global_search=self.is_global_search)

def _on_global_search_menu_checked(self, e):

self.is_global_search = e.IsChecked()

self._build_search_bar_menu()

if self.keyword:

pub.sendMessage('note.searching', keyword=self.keyword, is_global_search=self.is_global_search)

def _build_search_bar_menu(self):

menu = wx.Menu()

menu.AppendCheckItem(self._global_search_menu_id, '搜索所有笔记本').Check(self.is_global_search)

self.search_bar.SetMenu(menu)

if self.is_global_search:

self.search_bar.SetHint('搜索所有笔记本')

else:

self.search_bar.SetHint('搜索当前笔记本')

def set_count(self, count):

self.note_count = count

if self.keyword:

self.st_note_count.SetLabel(f'找到{self.note_count}条笔记')

else:

self.st_note_count.SetLabel(f'{self.note_count}条笔记')

def reset_search_bar(self):

self.search_bar.ChangeValue('')

@property

def keyword(self):

return self.search_bar.GetValue().strip()

全局搜索菜单依附于搜索框,每当勾选全局搜索选项时,就需要重新生成一个新菜单,否则勾选无效(这可能是 wx.SearchCtrl 的一个bug,有待确认)。

当搜索框的关键词变化或者全局菜单勾选时,就会发送 note.searching 消息。

编辑 views / list_panel.py,订阅 note.searching:

def __init__(self, parent):

# ...

self._notebook = None

pub.subscribe(self._on_note_searching, 'note.searching')

def _on_note_searching(self, keyword, is_global_search):

if keyword:

if is_global_search or not self._notebook:

notebook_id = None

else:

notebook_id = self._notebook.id

note_ids = self.searcher.search(keyword, notebook_id=notebook_id)

if note_ids:

notes = list(Note.select().where(Note.id.in_(note_ids)).order_by(self.header_panel.sort_option))

else:

notes = []

self._load(notes)

self.header_panel.set_count(len(notes))

else:

if self._notebook:

self._on_notebook_selected(self._notebook)

else:

self._on_root_selected()

def _on_notebook_selected(self, notebook):

# ...

self._notebook = notebook

self.header_panel.reset_search_bar()

def _on_root_selected(self):

# ...

self._notebook = None

self.header_panel.reset_search_bar()

这里保存了当前笔记本对象,是因为当搜索关键词清空后,需要重新加载当前笔记列表。

实现QuillJS查找功能

以上就实现了全文搜索功能,美中不足的是选中笔记时并没有高亮关键词。文本编辑器关键词高亮可以借助Quilljs提供的API实现。生成一个格式化文本节点,用于表示高亮文字,在Quilljs中叫做Blot

获取关键词在文本中所有的位置,生成一个索引数组。例如文本:abcabcd99ab,关键词为 ab,则索引数组为:[0, 3, 9]

有了索引和新建的Blot,就可以使用quill.format高亮关键词了

在 views / text_editor 目录下新建一个 searcher.js文件。

const Inline = Quill.import('blots/inline');

class MarkerBlot extends Inline {}

MarkerBlot.blotName = 'Marker';

MarkerBlot.className = 'marked';

MarkerBlot.tagName = 'div';

Quill.register(MarkerBlot);

class Searcher {

constructor(quill) {

this.quill = quill;

}

findAll(keyword) {

this.removeMark();

let keywordLength = keyword.length;

this.getIndexesOf(keyword).forEach(

index => this.quill.formatText(index, keywordLength, "Marker", true)

)

}

removeMark() {

this.quill.formatText(0, this.quill.getText().length, 'Marker', false);

}

getIndexesOf(keyword) {

if (keyword == null || keyword === "") {

return [];

}

keyword = keyword.toLowerCase();

let text = this.quill.getText().toLowerCase();

let keywordLength = keyword.length;

let startIndex = 0, curIndex, indexList = [];

while ((curIndex = text.indexOf(keyword, startIndex)) > -1) {

indexList.push(curIndex);

startIndex = curIndex + keywordLength;

}

return indexList;

}

}

编辑 quill.snow.css 文件,追加高亮效果:

.marked {

background-color: #fefb39;

display: inline;

}

编辑 index.html,引入 searcher.js:

编辑 core.js,添加搜索关键词方法:

let searcher = new Searcher(quill);

quill.findAll = function(keyword) {

searcher.findAll(keyword);

};

笔记高亮

编辑器层面的高亮效果已经实现了,该怎么整合到 TextEditor 组件中呢?我们知道,笔记加载实际上调用了 load_note 方法,更进一步来看,是订阅了 note.selected 消息,所以只需要在该消息中添加关键词数据即可。

修改 views / list_panel.py,给 _load 方法添加 keyword 参数:

def _load(self, notes, preserve_select=False, keyword=None):

if len(notes):

self.note_list_panel.replace(notes, preserve_select, keyword)

else:

self.note_list_panel.clear()

self._note_ids = list(map(lambda note: note.id, notes))

修改 views / note_list_panel.py:

def __init__(self, parent):

# ...

self.keyword = None

def replace(self, notes, preserve_select=False, keyword=None):

# ...

self.keyword = keyword

def clear(self):

# ...

self.keyword = None

def select(self, note):

# ...

pub.sendMessage('note.selected', note=self.selected_note, keyword=self.keyword)

修改 views / text_editor.py:

总结

如果将搜索相关逻辑转移到主窗体 MainFrame 中,会更加容易实现,因为MainFrame是最顶层组件,可以很容易获取到子组件的相关数据,有兴趣可以动手实践一下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值