使用 Weaviate 进行嵌入搜索

本笔记本将引导您完成一个简单的流程,以下载数据、对其进行嵌入,然后使用一系列向量数据库对其进行索引和搜索。这是客户希望在安全的环境中存储和搜索我们的嵌入以支持生产用例(如聊天机器人、主题建模等)的常见需求。

什么是向量数据库

向量数据库是一种用于存储、管理和搜索嵌入向量的数据库。近年来,由于人工智能在解决涉及自然语言、图像识别和其他非结构化数据方面的用例越来越有效,使用嵌入将非结构化数据(文本、音频、视频等)编码为供机器学习模型使用的向量已呈爆炸式增长。向量数据库已成为企业交付和扩展这些用例的有效解决方案。

为什么使用向量数据库

向量数据库使企业能够利用我们在此仓库中共享的许多嵌入用例(例如,问答、聊天机器人和推荐服务),并在安全、可扩展的环境中使用它们。我们的许多客户使用嵌入在小规模上解决他们的问题,但性能和安全性阻碍了他们投入生产——我们将向量数据库视为解决此问题的关键组成部分,在本指南中,我们将介绍嵌入文本数据、将其存储在向量数据库中以及使用它进行语义搜索的基础知识。

演示流程

演示流程如下:

  • 设置:导入包并设置任何必需的变量
  • 加载数据:加载数据集并使用 OpenAI 嵌入对其进行嵌入
  • Weaviate
    • 设置:在这里我们将设置 Weaviate 的 Python 客户端。更多详情请参见此处
    • 索引数据:我们将创建一个包含 title 搜索向量的索引
    • 搜索数据:我们将运行几次搜索以确认其有效

运行完此笔记本后,您应该对如何设置和使用向量数据库有一个基本的了解,然后可以继续进行更多利用我们嵌入的复杂用例。

设置

导入所需的库并设置我们要使用的嵌入模型。

# 我们需要安装 Weaviate 客户端
!pip install weaviate-client

# 安装 wget 以下载 zip 文件
!pip install wget
import openai

from typing import List, Iterator
import pandas as pd
import numpy as np
import os
import wget
from ast import literal_eval

# Weaviate 的 Python 客户端库
import weaviate

# 我已将其设置为我们新的嵌入模型,可以将其更改为您选择的嵌入模型
EMBEDDING_MODEL = "text-embedding-3-small"

# 忽略未关闭的 SSL 套接字警告 - 如果遇到这些错误,可以选择忽略
import warnings

warnings.filterwarnings(action="ignore", message="unclosed", category=ResourceWarning)
warnings.filterwarnings("ignore", category=DeprecationWarning)

加载数据

在本节中,我们将加载在此会话之前准备好的嵌入数据。

embeddings_url = 'https://cdn.openai.com/API/examples/data/vector_database_wikipedia_articles_embedded.zip'

# 该文件约 700 MB,因此需要一些时间
wget.download(embeddings_url)
import zipfile
with zipfile.ZipFile("vector_database_wikipedia_articles_embedded.zip","r") as zip_ref:
    zip_ref.extractall("../data")
article_df = pd.read_csv('../data/vector_database_wikipedia_articles_embedded.csv')
article_df.head()
id url title text title_vector content_vector vector_id
0 1 https://simple.wikipedia.org/wiki/April April April is the fourth month of the year in the J... [0.001009464613161981, -0.020700545981526375, ... [-0.011253940872848034, -0.013491976074874401,... 0
1 2 https://simple.wikipedia.org/wiki/August August August (Aug.) is the eighth month of the year ... [0.0009286514250561595, 0.000820168002974242, ... [0.0003609954728744924, 0.007262262050062418, ... 1
2 6 https://simple.wikipedia.org/wiki/Art Art Art is a creative activity that expresses imag... [0.003393713850528002, 0.0061537534929811954, ... [-0.004959689453244209, 0.015772193670272827, ... 2
3 8 https://simple.wikipedia.org/wiki/A A A or a is the first letter of the English alph... [0.0153952119871974, -0.013759135268628597, 0.... [0.024894846603274345, -0.022186409682035446, ... 3
4 9 https://simple.wikipedia.org/wiki/Air Air Air refers to the Earth's atmosphere. Air is a... [0.02224554680287838, -0.02044147066771984, -0... [0.021524671465158463, 0.018522677943110466, -... 4
# 将字符串中的向量读回列表
article_df['title_vector'] = article_df.title_vector.apply(literal_eval)
article_df['content_vector'] = article_df.content_vector.apply(literal_eval)

# 将 vector_id 设置为字符串
article_df['vector_id'] = article_df['vector_id'].apply(str)
article_df.info(show_counts=True)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 25000 entries, 0 to 24999
Data columns (total 7 columns):
 #   Column          Non-Null Count  Dtype
---  ------          --------------  -----
 0   id              25000 non-null  int64
 1   url             25000 non-null  object
 2   title           25000 non-null  object
 3   text            25000 non-null  object
 4   title_vector    25000 non-null  object
 5   content_vector  25000 non-null  object
 6   vector_id       25000 non-null  object
dtypes: int64(1), object(6)
memory usage: 1.3+ MB

Weaviate

我们将探索的另一个向量数据库选项是 Weaviate,它同时提供托管的 SaaS 选项和自托管的 开源 选项。由于我们已经看过了云向量数据库,这里我们将尝试自托管选项。

为此,我们将:

  • 设置 Weaviate 的本地部署
  • 在 Weaviate 中创建索引
  • 在那里存储我们的数据
  • 发起一些相似性搜索查询
  • 尝试一个实际用例

自带向量方法

在本食谱中,我们提供已生成向量的数据。这对于数据已向量化的场景非常有用。

使用 OpenAI 模块自动向量化

对于尚未向量化数据的情况,您可以将 OpenAI 的向量化任务委托给 Weaviate。 Weaviate 提供了一个内置模块 text2vec-openai,它负责在以下方面进行向量化:

  • 导入
  • 对于任何 CRUD 操作
  • 对于语义搜索

请查看 使用 Weaviate 和 OpenAI 模块入门食谱 以了解如何一步步导入和向量化数据。

设置

要本地运行 Weaviate,您需要 Docker。按照 Weaviate 文档 此处 中的说明,我们在本仓库中创建了一个示例 docker-compose.yml 文件,保存在 ./weaviate/docker-compose.yml

启动 Docker 后,您可以通过导航到 examples/vector_databases/weaviate/ 目录并运行 docker-compose up -d 来在本地启动 Weaviate。

SaaS

或者,您可以使用 Weaviate Cloud Service (WCS) 来创建免费的 Weaviate 集群。

  1. 创建一个免费账户和/或登录到 WCS
  2. 创建一个 Weaviate Cluster 并进行以下设置:
    • 沙盒:Sandbox Free
    • Weaviate 版本:使用默认值(最新)
    • OIDC 身份验证:Disabled
  3. 您的实例应该在一两分钟内准备就绪
  4. 记下 Cluster Id。该链接将带您进入集群的完整路径(稍后您将需要它来连接)。它应该类似于:https://your-project-name-suffix.weaviate.network
# 选项 #1 - 自托管 - Weaviate 开源
client = weaviate.Client(
    url="http://localhost:8080",
    additional_headers={
        "X-OpenAI-Api-Key": os.getenv("OPENAI_API_KEY")
    }
)
# 选项 #2 - SaaS - (Weaviate Cloud Service)
client = weaviate.Client(
    url="https://your-wcs-instance-name.weaviate.network",
    additional_headers={
        "X-OpenAI-Api-Key": os.getenv("OPENAI_API_KEY")
    }
)
client.is_ready()

索引数据

在 Weaviate 中,您创建 schema 来捕获您将搜索的每个实体。

在本例中,我们将创建一个名为 Article 的模式,其中包含上面提供的 title 向量,供我们搜索。

接下来的几个步骤将紧密遵循 Weaviate 提供的文档 此处

# 清除模式,以便我们可以重新创建它
client.schema.delete_all()
client.schema.get()

# 定义 Schema 对象以在 `title` 和 `content` 上使用 `text-embedding-3-small`,但跳过 `url`
article_schema = {
    "class": "Article",
    "description": "A collection of articles",
    "vectorizer": "text2vec-openai",
    "moduleConfig": {
        "text2vec-openai": {
          "model": "ada",
          "modelVersion": "002",
          "type": "text"
        }
    },
    "properties": [{
        "name": "title",
        "description": "Title of the article",
        "dataType": ["string"]
    },
    {
        "name": "content",
        "description": "Contents of the article",
        "dataType": ["text"],
        "moduleConfig": { "text2vec-openai": { "skip": True } }
    }]
}

# 添加 Article 模式
client.schema.create_class(article_schema)

# 获取模式以确保其正常工作
client.schema.get()
{'classes': [{'class': 'Article',
   'description': 'A collection of articles',
   'invertedIndexConfig': {'bm25': {'b': 0.75, 'k1': 1.2},
    'cleanupIntervalSeconds': 60,
    'stopwords': {'additions': None, 'preset': 'en', 'removals': None}},
   'moduleConfig': {'text2vec-openai': {'model': 'ada',
     'modelVersion': '002',
     'type': 'text',
     'vectorizeClassName': True}},
   'properties': [{'dataType': ['string'],
     'description': 'Title of the article',
     'moduleConfig': {'text2vec-openai': {'skip': False,
       'vectorizePropertyName': False}},
     'name': 'title',
     'tokenization': 'word'},
    {'dataType': ['text'],
     'description': 'Contents of the article',
     'moduleConfig': {'text2vec-openai': {'skip': True,
       'vectorizePropertyName': False}},
     'name': 'content',
     'tokenization': 'word'}],
   'replicationConfig': {'factor': 1},
   'shardingConfig': {'virtualPerPhysical': 128,
    'desiredCount': 1,
    'actualCount': 1,
    'desiredVirtualCount': 128,
    'actualVirtualCount': 128,
    'key': '_id',
    'strategy': 'hash',
    'function': 'murmur3'},
   'vectorIndexConfig': {'skip': False,
    'cleanupIntervalSeconds': 300,
    'maxConnections': 64,
    'efConstruction': 128,
    'ef': -1,
    'dynamicEfMin': 100,
    'dynamicEfMax': 500,
    'dynamicEfFactor': 8,
    'vectorCacheMaxObjects': 1000000000000,
    'flatSearchCutoff': 40000,
    'distance': 'cosine'},
   'vectorIndexType': 'hnsw',
   'vectorizer': 'text2vec-openai'}]}
### Step 1 - 配置 Weaviate Batch,它优化了批量 CRUD 操作
# - 启动批量大小为 100
# - 根据性能动态增加/减少
# - 添加超时重试以防万一

client.batch.configure(
    batch_size=100,
    dynamic=True,
    timeout_retries=3,
)
<weaviate.batch.crud_batch.Batch at 0x3f0ca0fa0>
### Step 2 - 导入数据

print("正在将数据及向量上传到 Article 模式..")

counter=0

with client.batch as batch:
    for k,v in article_df.iterrows():

        # 每 100 个对象打印一次更新消息
        if (counter %100 == 0):
            print(f"导入 {counter} / {len(article_df)} ")

        properties = {
            "title": v["title"],
            "content": v["text"]
        }

        vector = v["title_vector"]

        batch.add_data_object(properties, "Article", None, vector)
        counter = counter+1

print(f"导入 ({len(article_df)}) 文章完成")
正在将数据及向量上传到 Article 模式..
导入 0 / 25000
导入 100 / 25000
导入 200 / 25000
导入 300 / 25000
导入 400 / 25000
导入 500 / 25000
导入 600 / 25000
导入 700 / 25000
导入 800 / 25000
导入 900 / 25000
导入 1000 / 25000
导入 1100 / 25000
导入 1200 / 25000
导入 1300 / 25000
导入 1400 / 25000
导入 1500 / 25000
导入 1600 / 25000
导入 1700 / 25000
导入 1800 / 25000
导入 1900 / 25000
导入 2000 / 25000
导入 2100 / 25000
导入 2200 / 25000
导入 2300 / 25000
导入 2400 / 25000
导入 2500 / 25000
导入 2600 / 25000
导入 2700 / 25000
导入 2800 / 25000
导入 2900 / 25000
导入 3000 / 25000
导入 3100 / 25000
导入 3200 / 25000
导入 3300 / 25000
导入 3400 / 25000
导入 3500 / 25000
导入 3600 / 25000
导入 3700 / 25000
导入 3800 / 25000
导入 3900 / 25000
导入 4000 / 25000
导入 4100 / 25000
导入 4200 / 25000
导入 4300 / 25000
导入 4400 / 25000
导入 4500 / 25000
导入 4600 / 25000
导入 4700 / 25000
导入 4800 / 25000
导入 4900 / 25000
导入 5000 / 25000
导入 5100 / 25000
导入 5200 / 25000
导入 5300 / 25000
导入 5400 / 25000
导入 5500 / 25000
导入 5600 / 25000
导入 5700 / 25000
导入 5800 / 25000
导入 5900 / 25000
导入 6000 / 25000
导入 6100 / 25000
导入 6200 / 25000
导入 6300 / 25000
导入 6400 / 25000
导入 6500 / 25000
导入 6600 / 25000
导入 6700 / 25000
导入 6800 / 25000
导入 6900 / 25000
导入 7000 / 25000
导入 7100 / 25000
导入 7200 / 25000
导入 7300 / 25000
导入 7400 / 25000
导入 7500 / 25000
导入 7600 / 25000
导入 7700 / 25000
导入 7800 / 25000
导入 7900 / 25000
导入 8000 / 25000
导入 8100 / 25000
导入 8200 / 25000
导入 8300 / 25000
导入 8400 / 25000
导入 8500 / 25000
导入 8600 / 25000
导入 8700 / 25000
导入 8800 / 25000
导入 8900 / 25000
导入 9000 / 25000
导入 9100 / 25000
导入 9200 / 25000
导入 9300 / 25000
导入 9400 / 25000
导入 9500 / 25000
导入 9600 / 25000
导入 9700 / 25000
导入 9800 / 25000
导入 9900 / 25000
导入 10000 / 25000
导入 10100 / 25000
导入 10200 / 25000
导入 10300 / 25000
导入 10400 / 25000
导入 10500 / 25000
导入 10600 / 25000
导入 10700 / 25000
导入 10800 / 25000
导入 10900 / 25000
导入 11000 / 25000
导入 11100 / 25000
导入 11200 / 25000
导入 11300 / 25000
导入 11400 / 25000
导入 11500 / 25000
导入 11600 / 25000
导入 11700 / 25000
导入 11800 / 25000
导入 11900 / 25000
导入 12000 / 25000
导入 12100 / 25000
导入 12200 / 25000
导入 12300 / 25000
导入 12400 / 25000
导入 12500 / 25000
导入 12600 / 25000
导入 12700 / 25000
导入 12800 / 25000
导入 12900 / 25000
导入 13000 / 25000
导入 13100 / 25000
导入 13200 / 25000
导入 13300 / 25000
导入 13400 / 25000
导入 13500 / 25000
导入 13600 / 25000
导入 13700 / 25000
导入 13800 / 25000
导入 13900 / 25000
导入 14000 / 25000
导入 14100 / 25000
导入 14200 / 25000
导入 14300 / 25000
导入 14400 / 25000
导入 14500 / 25000
导入 14600 / 25000
导入 14700 / 25000
导入 14800 / 25000
导入 14900 / 25000
导入 15000 / 25000
导入 15100 / 25000
导入 15200 / 25000
导入 15300 / 25000
导入 15400 / 25000
导入 15500 / 25000
导入 15600 / 25000
导入 15700 / 25000
导入 15800 / 25000
导入 15900 / 25000
导入 16000 / 25000
导入 16100 / 25000
导入 16200 / 25000
导入 16300 / 25000
导入 16400 / 25000
导入 16500 / 25000
导入 16600 / 25000
导入 16700 / 25000
导入 16800 / 25000
导入 16900 / 25000
导入 17000 / 25000
导入 17100 / 25000
导入 17200 / 25000
导入 17300 / 25000
导入 17400 / 25000
导入 17500 / 25000
导入 17600 / 25000
导入 17700 / 25000
导入 17800 / 25000
导入 17900 / 25000
导入 18000 / 25000
导入 18100 / 25000
导入 18200 / 25000
导入 18300 / 25000
导入 18400 / 25000
导入 18500 / 25000
导入 18600 / 25000
导入 18700 / 25000
导入 18800 / 25000
导入 18900 / 25000
导入 19000 / 25000
导入 19100 / 25000
导入 19200 / 25000
导入 19300 / 25000
导入 19400 / 25000
导入 19500 / 25000
导入 19600 / 25000
导入 19700 / 25000
导入 19800 / 25000
导入 19900 / 25000
导入 20000 / 25000
导入 20100 / 25000
导入 20200 / 25000
导入 20300 / 25000
导入 20400 / 25000
导入 20500 / 25000
导入 20600 / 25000
导入 20700 / 25000
导入 20800 / 25000
导入 20900 / 25000
导入 21000 / 25000
导入 21100 / 25000
导入 21200 / 25000
导入 21300 / 25000
导入 21400 / 25000
导入 21500 / 25000
导入 21600 / 25000
导入 21700 / 25000
导入 21800 / 25000
导入 21900 / 25000
导入 22000 / 25000
导入 22100 / 25000
导入 22200 / 25000
导入 22300 / 25000
导入 22400 / 25000
导入 22500 / 25000
导入 22600 / 25000
导入 22700 / 25000
导入 22800 / 25000
导入 22900 / 25000
导入 23000 / 25000
导入 23100 / 25000
导入 23200 / 25000
导入 23300 / 25000
导入 23400 / 25000
导入 23500 / 25000
导入 23600 / 25000
导入 23700 / 25000
导入 23800 / 25000
导入 23900 / 25000
导入 24000 / 25000
导入 24100 / 25000
导入 24200 / 25000
导入 24300 / 25000
导入 24400 / 25000
导入 24500 / 25000
导入 24600 / 25000
导入 24700 / 25000
导入 24800 / 25000
导入 24900 / 25000
导入 (25000) 文章完成
# 测试所有数据是否已加载 – 获取对象计数
result = (
    client.query.aggregate("Article")
    .with_fields("meta { count }")
    .do()
)
print("对象计数: ", result["data"]["Aggregate"]["Article"])
对象计数:  [{'meta': {'count': 25000}}]
# 通过检查一个对象来测试一个文章是否工作正常
test_article = (
    client.query
    .get("Article", ["title", "content", "_additional {id}"])
    .with_limit(1)
    .do()
)["data"]["Get"]["Article"][0]

print(test_article["_additional"]["id"])
print(test_article["title"])
print(test_article["content"])
000393f2-1182-4e3d-abcf-4217eda64be0
Lago d'Origlio
Lago d'Origlio is a lake in the municipality of Origlio, in Ticino, Switzerland.

Lakes of Ticino

搜索数据

如上所述,我们将查询新的索引并根据与现有向量的接近程度获取结果

def query_weaviate(query, collection_name, top_k=20):

    # 从用户查询创建嵌入向量
    embedded_query = openai.Embedding.create(
        input=query,
        model=EMBEDDING_MODEL,
    )["data"][0]['embedding']

    near_vector = {"vector": embedded_query}

    # 使用向量化的用户查询查询输入模式
    query_result = (
        client.query
        .get(collection_name, ["title", "content", "_additional {certainty distance}"])
        .with_near_vector(near_vector)
        .with_limit(top_k)
        .do()
    )

    return query_result
query_result = query_weaviate("modern art in Europe", "Article")
counter = 0
for article in query_result["data"]["Get"]["Article"]:
    counter += 1
    print(f"{counter}. { article['title']} (确定性: {round(article['_additional']['certainty'],3) }) (距离: {round(article['_additional']['distance'],3) })")
1. Museum of Modern Art (确定性: 0.938) (距离: 0.125)
2. Western Europe (确定性: 0.934) (距离: 0.133)
3. Renaissance art (确定性: 0.932) (距离: 0.136)
4. Pop art (确定性: 0.93) (距离: 0.14)
5. Northern Europe (确定性: 0.927) (距离: 0.145)
6. Hellenistic art (确定性: 0.926) (距离: 0.147)
7. Modernist literature (确定性: 0.924) (距离: 0.153)
8. Art film (确定性: 0.922) (距离: 0.157)
9. Central Europe (确定性: 0.921) (距离: 0.157)
10. European (确定性: 0.921) (距离: 0.159)
11. Art (确定性: 0.921) (距离: 0.159)
12. Byzantine art (确定性: 0.92) (距离: 0.159)
13. Postmodernism (确定性: 0.92) (距离: 0.16)
14. Eastern Europe (确定性: 0.92) (距离: 0.161)
15. Europe (确定性: 0.919) (距离: 0.161)
16. Cubism (确定性: 0.919) (距离: 0.161)
17. Impressionism (确定性: 0.919) (距离: 0.162)
18. Bauhaus (确定性: 0.919) (距离: 0.162)
19. Expressionism (确定性: 0.918) (距离: 0.163)
20. Surrealism (确定性: 0.918) (距离: 0.163)
query_result = query_weaviate("Famous battles in Scottish history", "Article")
counter = 0
for article in query_result["data"]["Get"]["Article"]:
    counter += 1
    print(f"{counter}. {article['title']} (分数: {round(article['_additional']['certainty'],3) })")
1. Historic Scotland (分数: 0.946)
2. First War of Scottish Independence (分数: 0.946)
3. Battle of Bannockburn (分数: 0.946)
4. Wars of Scottish Independence (分数: 0.944)
5. Second War of Scottish Independence (分数: 0.94)
6. List of Scottish monarchs (分数: 0.937)
7. Scottish Borders (分数: 0.932)
8. Braveheart (分数: 0.929)
9. John of Scotland (分数: 0.929)
10. Guardians of Scotland (分数: 0.926)
11. Holyrood Abbey (分数: 0.925)
12. Scottish (分数: 0.925)
13. Scots (分数: 0.925)
14. Robert I of Scotland (分数: 0.924)
15. Scottish people (分数: 0.924)
16. Edinburgh Castle (分数: 0.924)
17. Alexander I of Scotland (分数: 0.924)
18. Robert Burns (分数: 0.924)
19. Battle of Bosworth Field (分数: 0.922)
20. David II of Scotland (分数: 0.922)

让 Weaviate 处理向量嵌入

Weaviate 有一个用于 OpenAI 的内置模块,它负责为您的查询和任何 CRUD 操作生成向量嵌入所需的步骤。

这允许您使用 with_near_text 过滤器运行向量查询,该过滤器使用您的 OPEN_API_KEY

def near_text_weaviate(query, collection_name):

    nearText = {
        "concepts": [query],
        "distance": 0.7,
    }

    properties = [
        "title", "content",
        "_additional {certainty distance}"
    ]

    query_result = (
        client.query
        .get(collection_name, properties)
        .with_near_text(nearText)
        .with_limit(20)
        .do()
    )["data"]["Get"][collection_name]

    print (f"返回的对象数: {len(query_result)}")

    return query_result
query_result = near_text_weaviate("modern art in Europe","Article")
counter = 0
for article in query_result:
    counter += 1
    print(f"{counter}. { article['title']} (确定性: {round(article['_additional']['certainty'],3) }) (距离: {round(article['_additional']['distance'],3) })")
返回的对象数: 20

1. Museum of Modern Art (确定性: 0.938) (距离: 0.125)
2. Western Europe (确定性: 0.934) (距离: 0.133)
3. Renaissance art (确定性: 0.932) (距离: 0.136)
4. Pop art (确定性: 0.93) (距离: 0.14)
5. Northern Europe (确定性: 0.927) (距离: 0.145)
6. Hellenistic art (确定性: 0.926) (距离: 0.147)
7. Modernist literature (确定性: 0.923) (距离: 0.153)
8. Art film (确定性: 0.922) (距离: 0.157)
9. Central Europe (确定性: 0.921) (距离: 0.157)
10. European (确定性: 0.921) (距离: 0.159)
11. Art (确定性: 0.921) (距离: 0.159)
12. Byzantine art (确定性: 0.92) (距离: 0.159)
13. Postmodernism (确定性: 0.92) (距离: 0.16)
14. Eastern Europe (确定性: 0.92) (距离: 0.161)
15. Europe (确定性: 0.919) (距离: 0.161)
16. Cubism (确定性: 0.919) (距离: 0.161)
17. Impressionism (确定性: 0.919) (距离: 0.162)
18. Bauhaus (确定性: 0.919) (距离: 0.162)
19. Surrealism (确定性: 0.918) (距离: 0.163)
20. Expressionism (确定性: 0.918) (距离: 0.163)
query_result = near_text_weaviate("Famous battles in Scottish history","Article")
counter = 0
for article in query_result:
    counter += 1
    print(f"{counter}. { article['title']} (确定性: {round(article['_additional']['certainty'],3) }) (距离: {round(article['_additional']['distance'],3) })")
返回的对象数: 20

1. Historic Scotland (确定性: 0.946) (距离: 0.107)
2. First War of Scottish Independence (确定性: 0.946) (距离: 0.108)
3. Battle of Bannockburn (确定性: 0.946) (距离: 0.109)
4. Wars of Scottish Independence (确定性: 0.944) (距离: 0.111)
5. Second War of Scottish Independence (确定性: 0.94) (距离: 0.121)
6. List of Scottish monarchs (确定性: 0.937) (距离: 0.127)
7. Scottish Borders (确定性: 0.932) (距离: 0.137)
8. Braveheart (确定性: 0.929) (距离: 0.141)
9. John of Scotland (确定性: 0.929) (距离: 0.142)
10. Guardians of Scotland (确定性: 0.926) (距离: 0.148)
11. Holyrood Abbey (确定性: 0.925) (距离: 0.15)
12. Scottish (确定性: 0.925) (距离: 0.15)
13. Scots (确定性: 0.925) (距离: 0.15)
14. Robert I of Scotland (确定性: 0.924) (距离: 0.151)
15. Scottish people (确定性: 0.924) (距离: 0.152)
16. Edinburgh Castle (确定性: 0.924) (距离: 0.153)
17. Alexander I of Scotland (确定性: 0.924) (距离: 0.153)
18. Robert Burns (确定性: 0.924) (距离: 0.153)
19. Battle of Bosworth Field (确定性: 0.922) (距离: 0.155)
20. David II of Scotland (确定性: 0.922) (距离: 0.157)

```