# 全文搜索 (Full-text Search)

> 分类代号：C.9  本章共 **3 个接口**  
> **可关闭特性**：`config.yaml` 里 `fullsearch.disable: true` 会让这组路由完全不注册；当 `full_search` 返回 `VjmapError(404)` 时即判定为"关闭"。

vjmap 内置 [bleve](https://github.com/blevesearch/bleve) 作为 CAD 图纸全文索引，主要用于搜"图上某段文字在哪儿"。典型使用场景：

- 用户在前端搜索框里输入"消防栓"，后端返回所有匹配到的文字 + bbox，前端画框高亮
- 按图元类型（单行/多行/属性注记/图层名/块名…）过滤
- 按工作区 / map_ver 隔离

---

## 快速索引

| 序号 | SDK 方法 | URL |
|----:|---------|-----|
| 58 | `full_search` / `fullSearch` | `POST /api/v1/service/fullsearch/search`（也支持 GET） |
| 59 | `full_search_add` / `fullSearchAdd` | `POST /api/v1/service/fullsearch/add` |
| 60 | `full_search_delete` / `fullSearchDelete` | `DELETE /api/v1/service/fullsearch/delete` |

---

## Document 数据结构

```jsonc
{
  "id":       "unique-id",          // 必填，全库唯一
  "map_ver":  "sys_zp_v1",          // 建议填，便于按图过滤
  "content":  "消防栓",              // 被索引的文本
  "type":     1,                    // 见下方 type 表
  "min_x":    0.0,                  // bbox
  "min_y":    0.0,
  "max_x":    1.0,
  "max_y":    1.0,
  "props":    {...},                // 任意附加属性
  "workspace":"_",                  // ⚠ 见下方坑位：默认工作区要显式写 "_"，不能留空
  "data":     "..."                  // 原始业务载荷（由你自己定义）
}
```

### `type` 常用值

| type | 说明 |
|-----:|------|
| 1 | 单行文字 |
| 2 | 多行文本 MText |
| 3 | 属性注记 Attribute |
| 4 | 块属性注记 |
| 5 | 图层名 |
| 6 | 块名 |
| 7 | 线型名 |
| 8 | 填充符号名 |
| 9 | 用户自定义 |

---

## 58 · 搜索 `fullsearch/search`

两种调用方式：

- **GET**：所有参数走 query，**适合简单关键词**。
- **POST**：所有参数走 JSON body，**支持复杂结构**（尤其 `bounding_box`）。

### 请求字段

| 字段 | 类型 | 说明 |
|------|------|------|
| `query` | string | 搜索关键词 |
| `map_ver` | string | 多个用 `,` 分隔；纯 mapid 自动取最新版本 |
| `type` | string | 类型过滤：`"1"` / `"1,2,5"` / `"1-3"` / `"1-3,6"` |
| `workspace` | string | 工作区，多个逗号分隔；缺省 `"_"`，`"*"` 查全部 |
| `limit` | int | 每页条数，默认 10 |
| `offset` | int | 分页偏移 |
| `fields` | string | 返回字段过滤，逗号分隔 |
| `facet` | bool | 是否按 `map_ver` 聚合 facet |
| `bounding_box` | string | `"minx,miny,maxx,maxy"` 或 `"[..]"` |
| `time_range` | string | `"2023-01-01~2023-12-31"` 或 `"7"`（最近 N 天） |

### 响应（实采）

```json
{
  "total": 0,
  "hits": [],
  "took": 2
}
```

`total` 是命中数量，`hits` 是文档数组（含 `id` / `content` / `score` / `fragments` 等 bleve 字段），`took` 是耗时毫秒。

### Python

```python
res = svc.full_search(
    query="消防栓",
    map_ver="proj_v1",
    type_="1,2",
    bounding_box="[0,0,1000,1000]",
    limit=20,
)
for hit in res.get("hits", []):
    print(hit["id"], hit.get("content"))
```

用 GET 走查询串：

```python
svc.full_search(query="消防栓", limit=5, use_get=True)
```

### Node.js

```javascript
const res = await svc.fullSearch('消防栓', {
  mapVer: 'proj_v1',
  type: '1,2',
  boundingBox: '[0,0,1000,1000]',
  limit: 20,
});
```

---

## 59 · 新增文档 `fullsearch/add`

### Body

```json
{
  "document": { "id":"...","map_ver":"...","content":"..." }
}
```

### 响应（实采）

```json
{
  "id": "nb_fs_1776832372",
  "message": "Document added successfully"
}
```

> **坑位**：`workspace` 留空 ≠ 默认工作区。自动同步会把空工作区名归一化为 `"_"` 再入库，但 `fullsearch/add` **不做归一化**——留空会以 `""` 入库；而搜索缺省固定追加 `workspace:"_"` 的 term 过滤，结果**默认搜索永远搜不到**。要么 add 时显式传 `"workspace":"_"`，要么搜索时传 `workspace:"*"`。

### Python

```python
svc.full_search_add({
    "id": "firehydrant_01",
    "map_ver": "proj_v1",
    "content": "消防栓",
    "type": 1,
    "workspace": "_",   # 显式写默认工作区，否则搜不到
    "min_x": 100, "min_y": 200, "max_x": 110, "max_y": 210,
})
```

> **批量导入**：本 SDK 只封装了单条 `full_search_add`。接口 `/fullsearch/batchAdd` 用同样的 POST JSON（`{"documents":[...]}`）；本次不做默认封装，可按需自行 `svc._request("POST", svc.service_url("fullsearch/batchAdd"), json_body={"documents":[...]})`。

---

## 60 · 删除文档 `fullsearch/delete`

注意是 **DELETE 带 JSON body**，而不是 `?id=`（gin 用 `ShouldBindJSON`）。

### Body

```json
{ "id": "firehydrant_01" }
```

### 响应（实采）

```json
{
  "id": "nb_fs_1776832372",
  "message": "Document deleted successfully"
}
```

### Python

```python
svc.full_search_delete("firehydrant_01")
```

---

## 常见错误

| HTTP 状态 | body 片段 | 含义 |
|-----------|-----------|------|
| 400 | `"error":"Invalid request parameters"` | JSON 反序列化失败 |
| 400 | `"error":"Document ID cannot be empty"` | 未填 `id` |
| 400 | `"error":"Invalid bounding box format"` | `bounding_box` 解析失败 |
| 404 | 整条路由不存在 | `fullsearch.disable:true`，功能关闭 |
| 500 | `"error":"Failed to initialize service"` | 后端索引损坏，看服务器日志 |

---

## 使用建议

1. **用 `map_ver` 隔离**：别把所有图纸的文字都塞一个索引里查全局，会拖慢。
2. **先 bbox 后全文**：多数场景只需搜当前视口，给 `bounding_box` 可大幅缩小候选集。
3. **批量构建走后台 job**：把"从 DWG 抽文字 → 批量 add"放到离线任务里；前端只做搜索。
4. **关闭索引也没关系**：SDK 的 `full_search` 会在 404 时抛 `VjmapError(404)`，应用层 catch 一下退化到普通查询即可。
5. **CJK 单字分词**：`query` 走 bleve match query + standard analyzer，中文按**单字**切分——搜"不存在的词"这类常见汉字组合会命中任何含其中某个字的文档。断言"无结果"请用纯拉丁乱码词；做精确匹配建议配合 `map_ver`/`type`/`bounding_box` 收敛候选集。

---
