ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Scrapy+ElasticSearch+Django构建小型全文搜索引擎实战

Scrapy+ElasticSearch+Django构建小型全文搜索引擎实战 简介基于Scrapy、ElasticSearch与Django的小型全文搜索引擎毕业设计项目面向计算机相关专业学生及需要快速搭建垂直搜索应用的开发者。资源围绕爬虫采集、索引构建、Web检索三条主线展开覆盖需求分析、技术选型、爬虫策略、索引映射、Django路由与视图等完整开发流程。压缩包共2000个文件、约88MB其中1600余个Python脚本承载爬虫调度与后端逻辑120余个HTML页面配合90余个JS文件组成交互前端另有少量CSS、JSON及配置文件辅助说明与部署。项目已录入完整源码与配套文档可直接用于理解搜索引擎各模块间协作方式也可作为毕业设计或课程项目的起点。目前已有136人学习下载适合在已有Python基础上希望系统掌握ScrapyElasticSearchDjango技术栈的中高级学习者。1. 小型全文搜索引擎Scrapy 抓数据ElasticSearch 建索引Django 做查询这个项目用 Scrapy、ElasticSearch、Django 三层搭出一个可运行的小型全文搜索引擎。Scrapy 负责抓取网页内容ElasticSearch 负责中文分词、索引和相关性排序Django 负责把查询结果做成页面接口。它针对性解决一类典型问题内容分散在多个页面中数据库 LIKE 查询既做不了中文分词也给不了标题和正文字段的加权排序而引入第三方搜索服务又会受制于数据安全和定制成本。这套组合适合毕业设计、课程设计也适合想做内部搜索原型的开发者。链路本身不长但每一层都有自己的坑下面按实际落地顺序把整条链路拆开从环境搭建、Mapping 设计一直讲到查询接口和排错。2. ElasticSearch 环境与 Mapping先定索引再谈爬虫2.1 ES 装在哪Windows 本机与 Docker 两种启动姿势很多读者的开发机是 Windows装 ElasticSearch 是经常卡住的第一步。项目要能在答辩或演示时不翻车ES 环境必须稳定。两种启动姿势我都跑过本机压缩包直接解压或者用 Docker Compose 起一个单节点。各有各的取舍。Windows 本机方式比较直接下载 ES 7.17 开头的版本解压后直接运行 bin/elasticsearch.bat等日志打出 started再访问 http://localhost:9200 能返回 JSON 节点信息就算成功。7.x 发行版自带 JDK不需要另外配置 JAVA_HOME。Docker 方式更适合反复重建环境和多人协作一张 compose 文件就能恢复整套环境。项目根目录放一个 docker-compose.ymlservices: es: image: docker.elastic.co/elasticsearch/elasticsearch:7.17.18 container_name: es_single environment: - discovery.typesingle-node - ES_JAVA_OPTS-Xms1g -Xmx1g - xpack.security.enabledfalse ports: - 9200:9200 volumes: - es_data:/usr/share/elasticsearch/data volumes: es_data:参数说明discovery.typesingle-node 是开发环境的关键参数。默认配置下 ES 会尝试发现集群中的其他节点单机部署时经常因此启动失败加上它就进入单节点模式xpack.security.enabledfalse 跳过安全认证。仅适合本机或局域网调试放到云服务器上必须改成 true 并配置密码ES_JAVA_OPTS 里 -Xms1g 和 -Xmx1g 保持一致避免 JVM 运行时反复扩缩堆内存。8G 机器上给 1G 够用给太多会挤占后面的爬虫和 Django 资源es_data 是命名卷删除容器后数据仍在。想彻底重置索引需要用 docker volume rm 删卷而不是只删容器启动后验证是否就绪curl -s http://localhost:9200 | jq .version.number返回类似 7.17.18 的版本号说明集群正常。ES 第一次启动要初始化数据目录耗时几十秒是正常的别一看没立刻返回就去翻进程。提示9200 端口被占用时在 config/elasticsearch.yml 里把 http.port 改成 9201同时把项目 settings.py 里的 ES_HOSTS 同步改掉否则 Django 会连错端口。我见过不少人踩同一个坑Windows 解压 ES 后直接放在 C:\Program Files 下启动时日志报 java.nio.file.AccessDeniedException原因是当前用户对系统目录没有写权限。ES 需要写 data 和 logs 目录解压路径最好选在用户目录或 D 盘否则要在 elasticsearch.yml 里指定 path.data 到可写位置。2.2 Mapping 设计ik 分词器、中文搜索与字段类型ES 的索引类似数据库表结构Mapping 决定字段怎么存、怎么分词。这个项目搜的是中文文章不能直接使用默认的 standard 分词器。standard 会把“毕业设计”切成“毕”“业”“设”“计”四个单字match 查询很难得到准确的相关性得分。所以要换 ik 分词器按最大匹配和智能模式切词。先装好 ik 插件然后创建索引# scripts/create_index.py import requests ES_URL http://localhost:9200 INDEX_NAME search_demo mapping { settings: { analysis: { analyzer: { ik_analyzer: { type: custom, tokenizer: ik_max_word } } } }, mappings: { properties: { title: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart, fields: { keyword: {type: keyword, ignore_above: 128} } }, content: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }, url: {type: keyword}, source: {type: keyword}, publish_date: {type: date, format: yyyy-MM-dd HH:mm:ss} } } } resp requests.put(f{ES_URL}/{INDEX_NAME}, jsonmapping) print(resp.status_code, resp.text)参数说明title 和 content 都定义为 textanalyzer 用 ik_max_word 在索引阶段尽量多切词提高召回率search_analyzer 用 ik_smart 在查询阶段减少噪声title 下嵌套 keyword 子字段是为了能排序。ES 7.x 里 text 字段默认不可排序只有 keyword 类型才能直接用于 sortignore_above 设为 128超过 128 字符的标题不进 keyword 子字段避免超长文本占内存字段选型参考表字段类型主要用途注意事项titletext keyword 子字段标题搜索、标题排序只有 keyword 子字段能排序contenttext正文关键词搜索必须清洗后索引否则 HTML 进倒排urlkeyword精确匹配和去重用作文档 _id 时保证唯一publish_datedate时间过滤或排序格式需和管道写入保持一致有一个容易被忽略的点ik 插件不是装完就自动生效到所有索引。ES 会先读现有索引的 mapping如果 mapping 里没有修改 analyzer新插件不会作用于老索引。调整映射的常见流程是先删除旧索引按新 mapping 重建最后重新同步数据。这个顺序问题后面避坑章会专门展开。2.3 先用 DSL 验证分词再联调爬虫索引建完后强烈建议先在 ES 的分词接口上把数据切一遍。这一步成本极低却能把“分词不对”和“爬虫没抓到数据”这两类问题分开。跳过这个验证直接去跑爬虫一旦搜不到排查范围会变成整条链路效率很低。用 curl 做一次分词验证curl -X POST http://localhost:9200/search_demo/_analyze?pretty \ -H Content-Type: application/json \ -d {analyzer: ik_max_word, text: 基于Scrapy的小型全文搜索引擎}返回的 tokens 里应该能看到“scrapy”“小型”“全文”“搜索引擎”这些词条。如果某些词缺失后续查询基本也查不到。也可以写一段 Python 把分词结果输出得更直观import requests text Django 后台数据推送 tokens requests.post( http://localhost:9200/search_demo/_analyze, json{analyzer: ik_max_word, text: text} ).json()[tokens] for t in tokens: print(t[token], t[start_offset], t[end_offset])这段输出的重点是切词粒度。词太碎会导致 term 查询匹配不到词太粗会导致很多无关结果混进来。遇到专有名词总被切碎可以在 ik 的自定义词典里加词。IK 的自定义词典配置在 ES plugins/ik/config/IKAnalyzer.cfg.xml指向一个 ext_dict 文本文件每行一个词。加完词以后不需要删索引但要重启 ES 让词典生效再重新验证一次。3. Scrapy 数据采集管道去重与字段清洗3.1 定义 Item 与 Pipeline抓下来不等于能入库Scrapy 抓下来的数据默认是一堆无序 dict直接塞进 ES 的常见后果HTML 标签混进 content 字段、空字符串被当作有效数据、日期格式不统一。项目里正确做法是先把字段统一收进 Item再在 Pipeline 里逐层处理。先看 items.py# items.py import scrapy class ArticleItem(scrapy.Item): url scrapy.Field() title scrapy.Field() content scrapy.Field() source scrapy.Field() publish_date scrapy.Field() crawl_time scrapy.Field()字段命名尽量和 ES Mapping 对齐。不同名也可以但会在后面多一层转换逻辑建议一开始就统一避免管道代码里堆满 if 分支。Pipeline 里做三件事清洗字段、校验必填、批量写 ES。先写清洗管道# pipelines.py import re from datetime import datetime from scrapy.exceptions import DropItem class CleanHtmlPipeline: def process_item(self, item, spider): content item.get(content) or content re.sub(rscript.*?/script, , content, flagsre.S) content re.sub(rstyle.*?/style, , content, flagsre.S) content re.sub(r[^], , content) content re.sub(r\s, , content).strip() item[content] content[:50000] if not item.get(url): raise DropItem(缺少 URL丢弃该条目) if not item.get(title): item[title] item[url] if not item.get(publish_date): item[publish_date] datetime.now().strftime(%Y-%m-%d %H:%M:%S) return item逻辑说明正则先把 script/style 标签整块删掉再剥掉残余 HTML 标签最后把多个空白压缩成单个空格。顺序不能反过来否则脚本内容会被当作文本索引缺失 URL 属于抓取错误直接丢弃避免脏数据污染倒排索引50000 字符截断是硬保险。不截断也能跑但过大文本会让高亮计算明显变慢接着是写 ES 的管道# pipelines.py from elasticsearch import Elasticsearch, helpers class ElasticsearchPipeline: def __init__(self, es_hosts): self.es Elasticsearch(es_hosts) classmethod def from_crawler(cls, crawler): return cls(es_hostscrawler.settings.get(ES_HOSTS, [http://localhost:9200])) def open_spider(self, spider): self.actions [] def process_item(self, item, spider): action { _index: search_demo, _id: item[url], _source: dict(item) } self.actions.append(action) if len(self.actions) 100: helpers.bulk(self.es, self.actions) self.actions.clear() return item def close_spider(self, spider): if self.actions: helpers.bulk(self.es, self.actions) self.actions.clear()参数说明ES_HOSTS 从 Scrapy settings 里读取常见做法是统一放到环境变量保持和 Django 端一致_id 用 URL 去重同一个链接被重复抓到时ES 会按相同 _id 覆盖不会产生重复文档100 条一批是保守值。4G 内存的机器上每批 100 最稳数据量很大时再考虑 500不要用一个超大列表一次性提交Scrapy 的并发参数也值得调。默认 CONCURRENT_REQUESTS 是 16抓到学校官网这类小站点容易给对方造成负担# settings.py CONCURRENT_REQUESTS 4 DOWNLOAD_DELAY 0.5 ROBOTSTXT_OBEY True DEFAULT_REQUEST_HEADERS { User-Agent: Mozilla/5.0 (search-demo/0.1) }参数说明CONCURRENT_REQUESTS4 降低并发避免把目标站点打挂DOWNLOAD_DELAY0.5 表示请求间有 500ms 间隔属于礼貌抓取ROBOTSTXT_OBEYTrue 是对 robots.txt 的尊重毕设项目建议保留自定义 UA 里写了项目名方便站点管理员通过日志找到来源3.2 增量抓取与去重URL 指纹比标题去重更可靠Scrapy 默认去重只看 URL。但很多内容站点存在同一篇正文挂多个 URL 的情况比如带推广参数的链接和原始链接指向同一篇文章。如果只靠 URL 去重ES 里就会积累大量重复文档。更可靠的去重方式是对正文取指纹正文前 200 个字符经过标准化后做 SHA1如果和已抓到的某个指纹相同就判定重复。实现如下# fingerprints.py import hashlib from scrapy.dupefilters import RFPDupeFilter class BodyFingerprintFilter(RFPDupeFilter): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.seen_signatures set() def request_seen(self, request): body_sample request.meta.get(body_sample, ) fp hashlib.sha1(body_sample.encode()).hexdigest() if fp in self.seen_signatures: return True self.seen_signatures.add(fp) return False爬虫侧要把正文样本塞进 metadef parse(self, response): text clean_text(response) body_sample text[:200] yield scrapy.Request( response.url, callbackself.parse_detail, meta{body_sample: body_sample}, dont_filterTrue )逻辑说明过滤器仍然保留 Scrapy 自身的 URL 去重逻辑。如果 URL 本身重复父类会先拦截URL 不同但正文相同指纹判断兜底指纹集合放在内存中进程重启就清空。项目扩大规模时把 set 换成 Redis set用 SADD 和 SISMEMBER 判断分布式环境也能用body_sample[:200] 不是固定值能概括正文即可。新闻标题通常在一行内200 字足够区分不同正文这个做法的局限是同一篇文章微调了措辞正文样本不同会被当成新文章。需要近似查重时再考虑 MinHash小型项目用 SHA1 摘要已经够了。3.3 动态页面场景Scrapy 配 Playwright 处理 iframe 内容很多后台系统用 iframe 内嵌详情页。这种结构用默认的 scrapy.Request 抓不到内容页面返回只包含空框架和脚本真正数据由另一个 URL 动态加载。处理这类页面常见做法是让 Scrapy 和 Playwright 配合。思路是在下载中间件里做判断某些请求需要渲染 JS就启动 Playwright 打开页面等 iframe 加载完取 iframe 的 HTML 封装成 HtmlResponse 返回给 Scrapy。# middlewares.py from playwright.sync_api import sync_playwright from scrapy.http import HtmlResponse class PlaywrightMiddleware: def process_request(self, request, spider): if not request.meta.get(render_js): return None with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(request.url, wait_untilnetworkidle) page.wait_for_selector(iframe#content-frame, timeout10000) target_frame None for f in page.frames: if detail in f.url: target_frame f break html target_frame.content() if target_frame else page.content() browser.close() return HtmlResponse( request.url, bodyhtml, encodingutf-8, requestrequest )逻辑说明page.frames 遍历所有 frame按 URL 特征找到真正的正文 frame。不要直接用 page.frames[1]iframe 顺序可能变化networkidle 会等所有网络请求完成对单页面够用并发量大时等待会拉长需要按站点响应速度调整返回 HtmlResponse 后Scrapy 后续 XPath 和 CSS 解析照常工作不影响已有解析函数参数说明timeout10000 单位是毫秒。目标站慢可以调到 20000但不能无限加大否则一个慢页面会阻塞整个爬虫线程headlessTrue 必须开着。桌面上弹浏览器窗口批量跑起来资源占用非常严重这种中间件方案适合快速解决问题。如果项目里这种页面很多建议用 scrapy-playwright 集成到下载器层每个请求通过 meta[playwright]True 控制异步性能更好。4. Django 查询层从 ES 取数再封装成页面接口4.1 用 elasticsearch-dsl 构造查询关键词、高亮、分页Django 不直接操作 ES大多数现目用 elasticsearch-dsl 把 JSON Query DSL 包装成 Python 对象。搜索逻辑放在独立的 services.py 里视图层保持简洁。services.py 典型实现# services.py from elasticsearch_dsl import Search from elasticsearch_dsl.query import MultiMatch ES_INDEX search_demo def search_articles(keyword, page1, page_size10): s Search(indexES_INDEX) s s.query( MultiMatch( querykeyword, fields[title^3, content], typebest_fields ) ) s s.highlight( title, content, pre_tags[mark], post_tags[/mark] ) s s.extra(sizepage_size, from_(page - 1) * page_size) response s.execute() results [] for hit in response: results.append({ url: hit.url, title: hit.title, content: hit.content, score: hit.meta.score, highlight: hit.meta.highlight.to_dict() if hasattr(hit.meta, highlight) else {} }) total response.hits.total.value if hasattr(response.hits.total, value) else response.hits.total return results, total逻辑说明fields[title^3, content] 里的 ^3 表示标题字段权重是正文的三倍。关键词同时出现在标题和正文时标题命中对总分贡献更大排序更贴近用户预期typebest_fields 对各个字段分别算分取最大值作为文档成绩。如果希望多个字段共同加分改用 cross_fieldshit.meta.highlight 是 ES 返回的高亮片段。Django 模板直接输出这个片段搜索词外面自带标签前端不用自己写高亮算法参数说明from_ 和 size 合起来实现分页page_size10 是文章列表的常规值。管理后台可以放宽到 20但不要轻易给到 50深分页成本会随页码迅速增加4.2 结果排序与字段映射把 _score 透传给前端ES 默认按 _score 降序返回结果也就是相关度最高的排最前面。调试时把 score 值一起传回前端能直观理解为什么某条排在最前面。视图层组织数据# views.py from django.shortcuts import render from .services import search_articles def search_view(request): keyword request.GET.get(q, ).strip() page max(1, int(request.GET.get(page, 1))) page_size 10 if not keyword: return render(request, search/index.html, {results: [], keyword: }) results, total search_articles(keyword, pagepage, page_sizepage_size) context { keyword: keyword, results: results, total: total, page: page, has_prev: page 1, has_next: page * page_size total, } return render(request, search/index.html, context)逻辑说明page 从 GET 参数读入外面套 max() 加默认值能挡住大多数异常输入has_prev / has_next 控制模板里上一页下一页按钮显隐total 来自 ES 的 hits.total普通查询是精确值。如果开启了 track_total_hits 限制total 可能变成近似值模板里应标注“约 X 条”排序这里有一个高频误用一旦在查询里显式加了 sort比如 sort[publish_date]_score 就不再参与排名结果会直接按时间倒序。想同时考虑相关性和时间用 function_scorefrom elasticsearch_dsl import Q from elasticsearch_dsl.function import ScriptScore s s.query( Q(function_score, queryMultiMatch(querykeyword, fields[title^3, content]), functions[ScriptScore(script{source: _score * 0.7 (doc[publish_date].value.getMillis() / 1000000000000.0) * 0.3})] ) )代码里的 0.7 和 0.3 是可调权重publish_date 的时间戳被压缩到合适量级避免日期完全盖过相关性。这个数值要按样本数据调能在答辩时说出调整依据比只会贴代码更有说服力。4.3 数据库与 ES 的同步策略Django Command 做全量重建和增量更新查询层写完后要解决同步文章内容一旦在数据库更新ES 索引可能还保留旧值。小项目不需要引入 Celery用 Django management command 定时同步就能满足。在 app 的 management/commands 目录下建 sync_es.py# management/commands/sync_es.py from datetime import timedelta from django.core.management.base import BaseCommand from django.utils import timezone from myapp.models import Article from myapp.services import index_article class Command(BaseCommand): help 把数据库里更新过的文章同步到 ElasticSearch def add_arguments(self, parser): parser.add_argument(--days, typeint, default1) parser.add_argument(--rebuild, actionstore_true, help全量重建索引) def handle(self, *args, **options): qs Article.objects.all() if not options[rebuild]: qs qs.filter(updated_at__gtetimezone.now() - timedelta(daysoptions[days])) count 0 for article in qs.iterator(chunk_size200): index_article(article) count 1 self.stdout.write(self.style.SUCCESS(f同步完成共 {count} 条))index_article 的逻辑放在 services.pydef index_article(article): doc { url: article.url, title: article.title, content: article.content, source: article.source, publish_date: article.publish_date.strftime(%Y-%m-%d %H:%M:%S), } es.index(indexsearch_demo, idarticle.url, bodydoc) return True逻辑说明--rebuild 用于修改 mapping 或清洗逻辑后全量刷索引。先删除旧索引再触发 rebuild是整套流程中最容易漏的一步--days 默认 1只同步最近一天更新的内容。配合 Windows 计划任务或 crontab每小时执行一次增量同步索引基本能跟上内容更新qs.iterator(chunk_size200) 类似数据库游标遍历大表时不加载全部对象到内存同步后抽查数据量curl -s http://localhost:9200/search_demo/_count | jq .countcount 和数据库文章数接近说明同步基本成功。差距明显就去查爬虫管道和同步任务日志逐层排查。5. 避坑指南启动顺序、分词边界与索引重建5.1 java.nio.file.AccessDeniedException数据目录权限不对现象Windows 本机或 Docker 启动 ES日志里出现 java.nio.file.AccessDeniedException进程很快退出。访问 9200 端口没有响应。原因ES 进程对数据目录没有写权限。Windows 上常见把压缩包解压到 Program FilesUAC 限制了当前用户写入Docker 上常见把宿主机只读目录挂载进容器的 /usr/share/elasticsearch/data。解决本机模式显式设置 path.data 和 path.logs避开受保护目录# elasticsearch.yml path.data: D:/es_data path.logs: D:/es_logsDocker 模式改用相对路径volumes: - ./es_data:/usr/share/elasticsearch/data启动后确认curl http://localhost:9200/_cluster/health?pretty返回 status 为 green 或 yellow 都正常red 则继续排查分片分配失败常见原因是磁盘空间不足或权限问题。修复后重启容器用 docker logs es_single 确认没有新的异常。5.2 中文搜索无结果ik 分词器没装或索引没重建现象Django 页面能打开查英文单词有结果输入“毕业设计”这类中文关键词返回 0 条。原因两种高频可能。一种是 ES 集群里没有安装 analysis-ik 插件mapping 里声明了 ik_analyzer 但实际没生效另一种是索引在安装 ik 插件之前就已创建mapping 一直用的 standard 分词器。修改 analyzer 配置不会热更新到已有索引。解决先确认插件bin/elasticsearch-plugin list没有 analysis-ik 就先安装插件再重启。接着删除旧索引并用新 mapping 重建curl -X DELETE http://localhost:9200/search_demo curl -X PUT http://localhost:9200/search_demo -H Content-Type: application/json -d mapping.json重建后再对中文做一次分词验证确认 tokens 里有完整词语再重新执行爬虫抓取或数据库同步。顺序反了等于白跑。补充一个隐藏变体mapping 里字段类型被设成 keyword。keyword 字段不做分词match 查询对它无效只能用 term 精确匹配。用户输入“毕业设计”keyword 字段只有完整字符串“毕业设计”才能命中输入“毕业”自然查不到。排查时重点看 mapping 里有没有 text 字段被误设成 keyword。5.3 Django 查询超时深分页与 scroll 游标泄漏现象搜索结果前 20 页正常翻到 50 页以后接口响应时间明显拉长甚至出现网关超时。ES 日志里偶尔出现 “Result window is too large” 异常。原因ES 默认允许 fromsize 最多查到 10000 条。from 超过这个值请求直接被拒。即便没超过深分页也要扫描并丢弃前面所有结果代价随页码线性增长。另一种常见问题是使用 scroll API 做全量导出时循环中断没有 clear_scroll游标长期占用内存。解决对外查询接口不要提供无限翻页深层结果改用 search_afters Search(indexES_INDEX) s s.query(MultiMatch(querykeyword, fields[title^3, content])) s s.sort(_id, publish_date) s s.extra(size50) result s.execute() search_after result[-1].meta.sort s2 Search(indexES_INDEX) s2 s2.query(MultiMatch(querykeyword, fields[title^3, content])) s2 s2.sort(_id, publish_date) s2 s2.extra(size50, search_aftersearch_after)逻辑说明search_after 用上一页最后一条的排序值定位下一页不计算 from页码再深性能也不退化_id 加 publish_date 的组合保证排序值唯一。只用 publish_date 排序时同一天的文档顺序不稳定翻页会出现重复或漏掉_id 本身不是短字符串时建议换一个数值 id 排序字段比如数据库主键需要导出全量数据时用 helpers.scan 避免手写游标清理from elasticsearch.helpers import scan for doc in scan(es, indexsearch_demo, query{match_all: {}}): process(doc)helpers.scan 内部自动管理游标异常时会清理比手写 scroll 加 while 循环省心。5.4 同步任务把脏数据写进 ES空标题和未清洗正文现象搜索结果里出现 title 为空的条目页面只能显示 URL或者 content 里能看到 function、var 等 JS 代码片段。原因上一轮爬虫在清洗管道没生效的情况下直接写了 ES。常见于调试时临时关闭 CleanHtmlPipeline或者同步命令直接读取了数据库里未清洗的原始字段。解决这类问题的根因是管道顺序混乱。检查 Scrapy setting 里的 ITEM_PIPELINES 顺序ITEM_PIPELINES { myproject.pipelines.CleanHtmlPipeline: 300, myproject.pipelines.ElasticsearchPipeline: 400, }数值小的先执行确保 CleanHtmlPipeline 永远在 ElasticsearchPipeline 之前。如果旧数据已经进了索引需要删索引重建后同步curl -X DELETE http://localhost:9200/search_demo python manage.py sync_es --rebuildES 里已有的脏数据不会因为新管道配置而消失必须删索引重建。开发环境没有风险线上环境先做备份。6. 把查询日志落库验证关键词命中率与索引质量6.1 用 SQLite 记录查询日志搜索做出来不算完还得能观察它到底表现如何。最简单有效的做法是把每次查询的关键词、返回数量、IP 和查询时间写进一张表。# models.py from django.db import models class SearchLog(models.Model): keyword models.CharField(max_length255, db_indexTrue) result_count models.IntegerField(default0) ip models.GenericIPAddressField(nullTrue, blankTrue) created_at models.DateTimeField(auto_now_addTrue)在搜索视图返回前插入一条记录SearchLog.objects.create( keywordkeyword, result_counttotal, iprequest.META.get(REMOTE_ADDR), )6.2 用日志反查索引短板积累两三天日志后用一条 SQL 看高频词和空结果词SELECT keyword, COUNT(*) AS cnt, AVG(result_count) AS avg_cnt FROM search_searchlog GROUP BY keyword ORDER BY cnt DESC LIMIT 20;avg_cnt 接近 0 的关键词说明索引没有覆盖用户想要的表达或分词不到位。返回结果数很大的词则要观察点击分布确认是不是噪声词。这个反馈循环能随时告诉你到底应该去调 ik 词典、补爬虫规则还是换查询字段。6.3 上线前固定一套验证脚本每次改完 ES 配置或爬虫管道我都会跑一段简短的验证脚本确认索引连通、中文分词正常、Django 接口能返回正确页面import requests base http://localhost:8000/search es http://localhost:9200 # 1. ES 集群存活 assert requests.get(f{es}/_cluster/health).status_code 200 # 2. 中文分词切出完整词 assert 毕业设计 in requests.post( f{es}/search_demo/_analyze, json{analyzer: ik_max_word, text: 毕业设计} ).text # 3. Django 接口返回关键词 resp requests.get(f{base}?q毕业设计) assert resp.status_code 200 assert 毕业设计 in resp.text print(全部通过)这三条断言能挡住绝大多数问题ES 没启动、分词器配置丢失、Django 服务端口切换。搜索项目真正难的不是某一层代码写不出来而是三层之间互相推卸责任搜不到词可能爬虫没抓到可能清洗丢了正文可能分词没切对也可能查询字段写错。有了这套基本检查至少能把问题收敛到一个环节再去深挖。从那以后我每次调完 ES 配置或改完管道都强制自己跑一遍脚本再提交代码。希望帮到你。本文还有配套的精品资源点击获取
返回列表