跳转至

Lookups:快速查询的二级索引

概览

Lookups 功能为 OMem 提供了基于哈希的二级索引,实现通过自定义键进行 O(1) 快速查询,无需向量嵌入的开销。

与向量索引(FAISS)执行语义相似度搜索不同,Lookups 对指定字段进行精确匹配,补充了你的语义记忆系统。

核心特性

多维索引:为不同的字段创建无限多个查找表(名字、地点、时间等)
自动维护:当项目合并或删除时,索引自动更新
内存高效:仅存储引用(主键),不复制数据
数据一致:无过期数据——查找表始终反映内存的当前状态

快速开始

1. 创建查找表

from ontomem import OMem, MergeStrategy
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

memory = OMem(
    memory_schema=Event,
    key_extractor=lambda x: x.id,
    llm_client=ChatOpenAI(model="gpt-4o"),
    embedder=OpenAIEmbeddings(),
    strategy_or_merger=MergeStrategy.MERGE_FIELD
)

# 为不同维度创建索引
memory.create_lookup("by_name", lambda x: x.char_name)
memory.create_lookup("by_location", lambda x: x.location)

2. 添加数据

events = [
    Event(id="evt_001", char_name="小红", location="厨房", ...),
    Event(id="evt_002", char_name="小明", location="厨房", ...),
    Event(id="evt_003", char_name="小红", location="客厅", ...),
]
memory.add(events)

3. 通过 Lookups 查询

# O(1) 快速按名字查询
xiaohong_events = memory.get_by_lookup("by_name", "小红")
# 返回: [evt_001, evt_003]

# O(1) 快速按位置查询
kitchen_events = memory.get_by_lookup("by_location", "厨房")
# 返回: [evt_001, evt_002]

# 结合向量搜索
memory.build_index()
semantic_results = memory.search("早上的活动", top_k=5)

API 参考

create_lookup(name: str, key_extractor: Callable[[T], Any]) -> None

创建一个新的二级查找表。

参数: - name: 这个查找表的唯一标识符(例如 "by_name"、"by_location") - key_extractor: 从实体中提取查找键的函数

异常: - ValueError: 如果已存在同名的查找表

示例:

memory.create_lookup("by_date", lambda x: x.timestamp[:10])  # YYYY-MM-DD

get_by_lookup(lookup_name: str, lookup_key: Any) -> List[T]

检索匹配查找键的项。

参数: - lookup_name: 要查询的查找表名称 - lookup_key: 要匹配的值

返回: - 匹配该键的实体列表。如果查找表或键不存在,返回空列表。

示例:

results = memory.get_by_lookup("by_date", "2024-01-15")

drop_lookup(name: str) -> bool

删除一个查找表。

参数: - name: 要删除的查找表名称

返回: - 删除成功返回 True,查找表不存在返回 False

示例:

memory.drop_lookup("by_location")

list_lookups() -> List[str]

列出所有已注册的查找表名称。

返回: - 当前活跃的查找表名称列表

示例:

print(memory.list_lookups())  # ['by_name', 'by_location', 'by_date']

合并时的数据一致性

Lookups 的关键特性是在项目合并时自动保持一致性

场景

  1. 你有 evt_001 在 "厨房"
  2. 查找表状态:"厨房" → {evt_001}
  3. 添加 evt_001 且 location="客厅"(同 ID,不同位置)
  4. 自动触发合并(采用较新的值)

结果

  • 旧的查找表条目 "厨房" → evt_001删除
  • 新的查找表条目 "客厅" → evt_001添加
  • 没有过期数据!

实现原理

OMem 使用快照策略

# 合并前:保存旧状态
old_item = storage[pk]

# 执行合并
merged_item = merger.merge([old_item, new_item])
storage[pk] = merged_item

# 更新所有查找表
# - 使用旧项状态从查找表中删除
# - 使用合并后的项状态添加到查找表

使用场景

1. 具有多维的时间序列数据

class GameEvent(BaseModel):
    id: str          # 主键
    char_name: str   # 谁
    location: str    # 哪里
    timestamp: str   # 什么时候
    action: str      # 做了什么

memory.create_lookup("by_character", lambda x: x.char_name)
memory.create_lookup("by_location", lambda x: x.location)
memory.create_lookup("by_hour", lambda x: x.timestamp.split(':')[0])

# 查找涉及某个角色的所有事件
character_history = memory.get_by_lookup("by_character", "小红")

# 查找某个地点发生的所有事件
location_events = memory.get_by_lookup("by_location", "厨房")

# 查找早上时间段的所有事件
morning_events = memory.get_by_lookup("by_hour", "08")

2. 用户档案管理

class UserProfile(BaseModel):
    user_id: str
    email: str
    company: str
    department: str
    skills: list[str]

memory.create_lookup("by_email", lambda x: x.email)
memory.create_lookup("by_company", lambda x: x.company)
memory.create_lookup("by_department", lambda x: f"{x.company}:{x.department}")

# 快速查找
user = memory.get_by_lookup("by_email", "alice@example.com")[0]
company_users = memory.get_by_lookup("by_company", "TechCorp")
dept_users = memory.get_by_lookup("by_department", "TechCorp:Engineering")

3. 分层数据

# 复合键用于分层查询
memory.create_lookup(
    "by_location_hour",
    lambda x: f"{x.location}:{x.timestamp.split(':')[0]}"
)

# 查询:厨房在 08:00 的事件
results = memory.get_by_lookup("by_location_hour", "厨房:08")

性能特征

操作 复杂度 备注
创建查找表 O(n) 一次性成本(n = 现有项数)
查询 O(1) 哈希查找
检索匹配项 O(m) m = 匹配项数
添加项 O(l) l = 查找表数量
删除项 O(l) 在所有查找表上清理
合并项 O(l) 每个查找表上删除旧 + 添加新

内存开销

Lookups 仅存储引用(主键),最小化内存:

场景:1,000,000 个项目,10 个查找表,每个查找表 100 个唯一值
内存:~4-10 MB(0.001-0.01% 的存储)

相比之下,向量索引通常消耗 10-50% 的存储空间。

最佳实践

✅ 应该做

  • 在添加大量数据之前创建查找表
  • 精确匹配特定字段使用查找表
  • 将查找表与向量搜索结合进行强大的查询
  • 删除未使用的查找表以节省内存
  • 对查找键使用可哈希的类型(str, int, tuple)

❌ 不应该做

  • 使用查找表进行模糊/部分匹配(使用向量搜索代替)
  • 为每个可能的字段创建查找表(要有选择性)
  • 在查找表中存储 None 值(会跳过并显示警告)
  • 依赖查找表进行子字符串匹配(不支持)

将 Lookups 与向量搜索结合

同时获得两者的优势:

# 步骤 1:使用 Lookups 进行精确过滤
kitchen_events = memory.get_by_lookup("by_location", "厨房")

# 步骤 2:对过滤结果进行语义搜索
relevant_kitchen_events = [
    e for e in kitchen_events 
    if e in memory.search("烹饪活动", top_k=100)
]

# 或反过来
relevant_in_memory = memory.search("烹饪", top_k=50)
kitchen_relevant = [
    e for e in relevant_in_memory
    if e in memory.get_by_lookup("by_location", "厨房")
]

常见问题

Q: "Lookup 'by_name' already exists"(查找表已存在)

A: 你尝试创建一个已存在的同名查找表。先使用 drop_lookup() 或使用不同的名称。

memory.drop_lookup("by_name")
memory.create_lookup("by_name", new_extractor)

Q: 我的查找表返回空结果

A: 常见原因: 1. 键值拼写错误:检查你使用的确切值 2. 提取器不匹配:验证提取器返回你查询的值 3. None 值:某些项的提取器可能返回 None

Q: 合并后查找表不一致

这不应该发生。如果发生了,请提交 issue。查找表在每次合并时自动更新。

迁移指南

如果你之前进行手动索引:

之前:

# 手动索引维护
name_index = {}
for item in memory.items:
    if item.name not in name_index:
        name_index[item.name] = []
    name_index[item.name].append(item)

# 手动查询
xiaohong = name_index.get("小红", [])

之后:

# 自动使用 Lookups
memory.create_lookup("by_name", lambda x: x.name)
xiaohong = memory.get_by_lookup("by_name", "小红")

Lookups 功能自动处理所有维护工作,包括在合并和删除期间的更新。

参考资源