ทำการรวมด้วยการสืบค้นย่อย

ภาพรวม

รุ่น Firestore Enterprise รองรับการรวมสไตล์เชิงสัมพันธ์ผ่านการสืบค้นย่อยที่สัมพันธ์กัน ไม่เหมือนกับฐานข้อมูล NoSQL หลายรายการที่มักต้อง ยกเลิกการทำให้ข้อมูลเป็นปกติหรือทำการขอฝั่งไคลเอ็นต์หลายรายการ คิวรีย่อยช่วยให้คุณ รวมและรวบรวมข้อมูลจากคอลเล็กชันหรือคอลเล็กชันย่อยที่เกี่ยวข้อง ได้โดยตรงบนเซิร์ฟเวอร์

การค้นหาย่อยคือนิพจน์ที่เรียกใช้ไปป์ไลน์ที่ซ้อนกันสำหรับทุกเอกสาร ที่ประมวลผลโดยการค้นหาภายนอก ซึ่งช่วยให้ใช้รูปแบบการดึงข้อมูลที่ซับซ้อนได้ เช่น การดึงข้อมูลเอกสารพร้อมกับรายการในคอลเล็กชันย่อยที่เกี่ยวข้อง หรือ การรวมข้อมูลที่ลิงก์กันอย่างมีตรรกะในคอลเล็กชันระดับรูทที่แตกต่างกัน

แนวคิด

ส่วนนี้จะแนะนําแนวคิดหลักเบื้องหลังการใช้คําสั่งย่อยเพื่อ ทําการรวมในการดําเนินการไปป์ไลน์

Subquery เป็นนิพจน์

Subquery ไม่ใช่ขั้นตอนระดับบนสุด แต่เป็นนิพจน์ที่ ใช้ได้ในทุกขั้นตอนที่ยอมรับนิพจน์ เช่น select(...) add_fields(...) where(...) หรือ sort(...)

Cloud Firestore รองรับคําค้นหาย่อย 3 ประเภท ได้แก่

  • การสืบค้นย่อยของอาร์เรย์: สร้างชุดผลลัพธ์ทั้งหมดของการสืบค้นย่อยเป็นอาร์เรย์ของเอกสาร
  • การสืบค้นย่อยแบบสเกลาร์: ประเมินเป็นค่าเดียว เช่น จำนวน ค่าเฉลี่ย หรือฟิลด์ที่เฉพาะเจาะจงจากเอกสารที่เกี่ยวข้อง
  • subcollection(...) Subquery: การรวมที่ง่ายขึ้นสำหรับความสัมพันธ์แบบหนึ่งต่อหลายรายการ ระหว่างรายการหลักกับรายการย่อย

ขอบเขตและตัวแปร

เมื่อเขียนการรวม คุณมักจะต้องอ้างอิงฟิลด์จากเอกสาร "ภายนอก" (ระดับบนสุด) ในการค้นหาย่อยที่ซ้อนกัน เพื่อเชื่อมขอบเขตเหล่านี้ ให้ใช้ขั้นตอน let(...) (เรียกว่า define(...) ใน SDK บางรายการ) เพื่อกำหนดตัวแปรในขอบเขตระดับบนสุด ซึ่งจะอ้างอิงได้ในการค้นหาย่อยโดยใช้ฟังก์ชัน variable(...)

ไวยากรณ์

ส่วนต่อไปนี้จะแสดงภาพรวมของไวยากรณ์สำหรับการรวม

let(...) ขั้นตอน

ขั้นตอนlet(...) (เรียกว่า define(...) ใน SDK บางรายการ) เป็นขั้นตอนที่ไม่ใช่การกรองซึ่งนำข้อมูลจากขอบเขตระดับบนมาไว้ในตัวแปรที่มีชื่ออย่างชัดเจนเพื่อใช้ในขอบเขตที่ซ้อนกันในภายหลัง

การสืบค้นย่อยของอาร์เรย์

การค้นหาย่อยของอาร์เรย์เป็นกรณีพิเศษของการค้นหาย่อยของนิพจน์ที่สร้างชุดผลลัพธ์ทั้งหมดของการค้นหาย่อยเป็นอาร์เรย์ หากการค้นหาย่อยแสดงผลเป็น 0 แถว การค้นหาย่อยจะประเมินเป็นอาร์เรย์ว่าง การค้นหาย่อยจะไม่แสดงผลอาร์เรย์ null การค้นหาดังกล่าวมีประโยชน์เมื่อต้องใช้ผลลัพธ์ทั้งหมดในผลลัพธ์สุดท้าย เช่น เมื่อสร้างคอลเล็กชันที่ซ้อนกันหรือสัมพันธ์กัน

การค้นหาสามารถกรอง จัดเรียง และรวบรวมในคำสั่งย่อยเพื่อลดปริมาณ ข้อมูลที่ต้องดึงและส่งคืนเพื่อช่วยลดค่าใช้จ่ายของการค้นหาได้ ระบบจะพิจารณาลำดับของคำสั่งย่อย ซึ่งหมายความว่าขั้นตอน sort(...) ในคำสั่งย่อยจะควบคุมลำดับของผลลัพธ์ในอาร์เรย์สุดท้าย

ใช้ Wrapper ของ toArrayExpression() SDK เพื่อแปลงการค้นหาเป็นอาร์เรย์

การสืบค้นย่อยแบบสเกลาร์

มักใช้คิวรีย่อยแบบสเกลาร์ในขั้นตอน select(...) หรือ where(...) เพื่ออนุญาตการกรองหรือแสดงผล ผลลัพธ์ของคิวรีย่อยโดยไม่ต้องสร้างคิวรีแบบเต็มโดยตรง

Subquery แบบสเกลาร์ที่ให้ผลลัพธ์เป็น 0 จะประเมินเป็น null เอง ในขณะที่ Subquery ที่ประเมินเป็นองค์ประกอบหลายรายการจะทำให้เกิดข้อผิดพลาดรันไทม์

เมื่อคิวรีย่อยแบบสเกลาร์สร้างฟิลด์เดียวต่อผลลัพธ์ ระบบจะยกระดับฟิลด์นั้นให้เป็นผลลัพธ์ระดับบนสุดสำหรับคิวรีย่อย โดยมักจะเห็นได้เมื่อคำสั่งย่อยลงท้ายด้วย select(field("user_name")) หรือ aggregate(countAll().as("total")) ซึ่งสคีมาของคำสั่งย่อยเป็นเพียงฟิลด์เดียว มิฉะนั้น เมื่อคิวรีย่อยสร้างฟิลด์ได้หลายรายการ ระบบจะ ห่อฟิลด์เหล่านั้นไว้ในแมป

ใช้ toScalarExpression() SDK Wrapper เพื่อแปลงการค้นหาเป็นนิพจน์สเกลาร์

subcollection(...) คำสั่งย่อย

แม้จะเสนอเป็นสเตจ แต่สเตจอินพุต subcollection(...) ก็ช่วยให้Cloud Firestoreสามารถเข้าร่วมโมเดลข้อมูลแบบลำดับชั้นได้ ในโมเดลแบบลำดับชั้น คำค้นหามักจะต้องดึงข้อมูลเอกสารพร้อมกับข้อมูลจากคอลเล็กชันย่อยของตัวเอง แม้ว่าคุณจะทำได้โดยใช้สเตจอินพุต collection_group(...) ตามด้วยตัวกรองในการอ้างอิงระดับบน แต่ subcollection(...) มีไวยากรณ์ที่กระชับกว่ามาก

นอกเหนือจากเงื่อนไขการรวมโดยนัยแล้ว การดำเนินการนี้จะคล้ายกับ คำสั่งย่อยของอาร์เรย์ โดยจะแสดงผลลัพธ์ที่ว่างเปล่าหากไม่มีเอกสารที่ตรงกัน แม้ว่าจะไม่มีคอลเล็กชันที่ซ้อนกันอยู่ก็ตาม

ซึ่งเป็นไวยากรณ์ที่กระชับโดยพื้นฐาน นั่นคือจะใช้__name__ของ เอกสารในขอบเขตนอกเป็นคีย์การรวมโดยอัตโนมัติเพื่อแก้ปัญหาความสัมพันธ์แบบลำดับชั้น จึงเป็นวิธีที่แนะนำในการค้นหาในคอลเล็กชันที่ลิงก์ในความสัมพันธ์แบบหลักกับย่อย

ตัวอย่าง

ตัวอย่างข้อมูล

ต่อไปนี้จะโหลดชุดข้อมูลทดสอบเพื่อใช้ในตัวอย่างทั้งหมดที่ตามมา

Node.js

// Load set of cities.
const cities = collection(db, "cities");

await setDoc(doc(cities, "SF"), {
  name: "San Francisco",
  state: "CA",
  country: "USA",
});
await setDoc(doc(cities, "LA"), {
  name: "Los Angeles",
  state: "CA",
  country: "USA"
});
await setDoc(doc(cities, "DC"), {
  name: "Washington, D.C.",
  state: null,
  country: "USA"
});
await setDoc(doc(cities, "TOK"), {
  name: "Tokyo",
  state: null,
  country: "Japan"
});

// Load restaurants in various cities.
const sfRestaurants = collection(db, "cities", "SF", "restaurants");
const laRestaurants = collection(db, "cities", "LA", "restaurants");
const dcRestaurants = collection(db, "cities", "DC", "restaurants");

const rest1 = await addDoc(sfRestaurants, {
  name: "Golden Gate Pizza",
  type: "pizza",
  owner_id: "Mario Rossi"
});
const rest2 = await addDoc(sfRestaurants, {
  name: "Bay Area Burger",
  type: "burger",
  owner_id: "Sarah Jenkins"
});
const rest3 = await addDoc(sfRestaurants, {
  name: "Sunset Taco",
  type: "mexican",
  owner_id: "Edward"
});

const rest4 = await addDoc(laRestaurants, {
  name: "Hollywood Sushi",
  type: "sushi",
  owner_id: "Ken Kenji"
});
const rest5 = await addDoc(laRestaurants, {
  name: "Venice Pizza",
  type: "pizza",
  owner_id: "Luigi Romano"
});

const rest6 = await addDoc(dcRestaurants, {
  name: "Capitol Tacos",
  type: "mexican",
  owner_id: "Maria Garcia"
});
const rest7 = await addDoc(dcRestaurants, {
  name: "Georgetown Coffee",
  type: "cafe",
  owner_id: "David Kim"
});

// Load collection of reviews.
const reviews = collection(db, "reviews");

await addDoc(reviews, { restaurant: rest1, rating: 5, reviewer_id "Alice" });
await addDoc(reviews, { restaurant: rest1, rating: 4, reviewer_id "Bob" });
await addDoc(reviews, { restaurant: rest2, rating: 4, reviewer_id "Charlie" });
await addDoc(reviews, { restaurant: rest3, rating: 5, reviewer_id "Diana" });
await addDoc(reviews, { restaurant: rest3, rating: 4, reviewer_id "Edward" });
await addDoc(reviews, { restaurant: rest3, rating: 4, reviewer_id "Fiona" });
// rest4 has 0 reviews
await addDoc(reviews, { restaurant: rest5, rating: 3, reviewer_id "George" });
await addDoc(reviews, { restaurant: rest6, rating: 5, reviewer_id "Hannah" });
await addDoc(reviews, { restaurant: rest6, rating: 4, reviewer_id "Ian" });
await addDoc(reviews, { restaurant: rest7, rating: 5, reviewer_id "Julia" });

ค้นหาเอกสารในคอลเล็กชันอื่น

การค้นหาต่อไปนี้ในกลุ่มคอลเล็กชัน reviews จะทำการค้นหาในกลุ่มคอลเล็กชัน restaurant โดยใช้การอ้างอิงคีย์หลัก

Node.js

let results = await execute(db.pipeline()
  .collectionGroup("reviews")
  .define(field("restaurant").as("restaurant_name"))
  .addFields(db.pipeline()
    .collectionGroup("restaurant")
    .where(field("__name__").equal(variable("restaurant_name")))
    .select("name", "type")
    .toScalarExpression()
    .as("restaurant")));

การตอบกลับ

{
  rating: 5,
  reviewer_id "Alice",
  restaurant: { name: "Golden Gate Pizza", type: "pizza" }
},
{
  rating: 4,
  reviewer_id "Bob",
  restaurant: { name: "Golden Gate Pizza", type: "pizza" }
},
{
  rating: 4,
  reviewer_id "Charlie",
  restaurant: { name: "Bay Area Burger", type: "burger" }
},
{
  rating: 5,
  reviewer_id "Diana",
  restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
  rating: 4,
  reviewer_id "Edward",
  restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
  rating: 4,
  reviewer_id "Fiona",
  restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
  rating: 3,
  reviewer_id "George",
  restaurant: { name: "Venice Pizza", type: "pizza" }
},
{
  rating: 5,
  reviewer_id "Hannah",
  restaurant: { name: "Capitol Tacos", type: "mexican" }
},
{
  rating: 4,
  reviewer_id "Ian",
  restaurant: { name: "Capitol Tacos", type: "mexican" }
},
{
  rating: 5,
  reviewer_id "Julia",
  restaurant: { name: "Georgetown Coffee", type: "cafe" }
}

รวมคอลเล็กชันหลายรายการ

การค้นหาต่อไปนี้จะดึงร้านพิซซ่าทั้งหมดจากrestaurantsคอลเล็กชัน กลุ่ม และใช้การค้นหาย่อยแบบอาร์เรย์เพื่อดึงและฝังรีวิวที่เกี่ยวข้อง ลงในการตอบกลับโดยตรง

Node.js

let results = await execute(db.pipeline()
  .collectionGroup("restaurants")
  .where(field("type").equal("pizza"))
  .define(field("__name__").as("restaurant_name"))
  .select(
    field("name"),
    db.pipeline()
      .collectionGroup("reviews")
      .where(field("restaurant").equal(variable("restaurant_name")))
      .select("rating", "reviewer_id")
      .toArrayExpression()
      .as("reviews")));

การตอบกลับ

{
  name: "Golden Gate Pizza",
  reviews: [
    { rating: 5, reviewer_id "Alice" },
    { rating: 4, reviewer_id "Bob" }
  ]
},
{
  name: "Venice Pizza",
  type: "pizza",
  owner_id: "Luigi Romano",
  reviews: [
    { rating: 3, reviewer_id "George" }
  ]
}

รวบรวมข้อมูลในหลายคอลเล็กชัน

การค้นหาต่อไปนี้ในกลุ่มคอลเล็กชัน restaurants ใช้การค้นหาข้อย่อยที่สัมพันธ์กันเพื่อรับคะแนนเฉลี่ยของร้านอาหารแต่ละร้านจากกลุ่มคอลเล็กชัน reviews

Node.js

let results = await execute(db.pipeline()
  .collectionGroup("restaurants")
  .where(field("type").equal("pizza"))
  .define(field("__name__").as("restaurant_name"))
  .select(
    field("name"),
    db.pipeline()
      .collectionGroup("reviews")
      .where(field("restaurant").equal(variable("restaurant_name")))
      .aggregate(average("rating").as("avg_rating"))
      .toScalarExpression()
      .as("avg_rating")));

การตอบกลับ

{
  name: "Golden Gate Pizza",
  avg_rating: 4.5
},
{
  name: "Venice Pizza",
  avg_rating: 3.0
}

Top-N ต่อกลุ่ม (คำสั่งย่อยที่มี LIMIT)

การค้นหาต่อไปนี้จะดึงข้อมูลเอกสารทั้งหมดจากคอลเล็กชันrestaurants กลุ่ม และใช้การค้นหาย่อยที่สัมพันธ์กันเพื่อดึงข้อมูลรีวิว 2 รายการที่ได้รับคะแนนสูงสุด สำหรับร้านอาหารแต่ละร้าน

เพื่อให้มั่นใจว่าอาร์เรย์ของรีวิวจะไม่ใหญ่เกินไปและไม่เกินขีดจำกัดหน่วยความจำของคำค้นหา

Node.js

let results = await execute(db.pipeline()
  .collectionGroup("restaurants")
  .define(field("__name__").as("restaurant_name"))
  .select(
    field("name"),
    db.pipeline()
      .collectionGroup("reviews")
      .where(field("restaurant").equal(variable("restaurant_name")))
      .sort(field("rating").descending())
      .limit(2)
      .select("rating", "reviewer_id")
      .toArrayExpression()
      .as("top_reviews")));

การตอบกลับ

{
  name: "Golden Gate Pizza",
  top_reviews: [
    { rating: 5, reviewer_id "Alice" },
    { rating: 4, reviewer_id "Bob" }
  ]
},
{
  name: "Bay Area Burger",
  top_reviews: [
    { rating: 4, reviewer_id "Charlie" }
  ]
},
{
  name: "Sunset Taco",
  top_reviews: [
    { rating: 5, reviewer_id "Diana" },
    { rating: 4, reviewer_id "Edward" }
  ]
},
{
  name: "Hollywood Sushi",
  top_reviews: []
},
{
  name: "Venice Pizza",
  top_reviews: [
    { rating: 3, reviewer_id "George" }
  ]
},
{
  name: "Capitol Tacos",
  top_reviews: [
    { rating: 5, reviewer_id "Hannah" },
    { rating: 4, reviewer_id "Ian" }
  ]
},
{
  name: "Georgetown Coffee",
  top_reviews: [
    { rating: 5, reviewer_id "Julia" }
  ]
}

รวมคอลเล็กชันย่อย

คำค้นหาต่อไปนี้จะสแกนคอลเล็กชัน cities และใช้ขั้นตอน subcollection(...) เพื่อรวมเอกสารจากคอลเล็กชันที่ซ้อนกันโดยนัยเพื่อค้นหาจำนวนร้านอาหารต่อเมือง

Node.js

let results = await execute(db.pipeline()
  .collection("cities")
  .addFields(subcollection("restaurants")
    .toArrayExpression()
    .length()
    .as("restaurant_count")));

การตอบกลับ

{
  __name__: cities/SF,
  name: "San Francisco",
  state: "CA",
  country: "USA",
  restaurant_count: 3
},
{
  __name__: cities/LA,
  name: "Los Angeles",
  state: "CA",
  country: "USA",
  restaurant_count: 2
},
{
  __name__: cities/DC,
  name: "Washington, D.C.",
  state: null,
  country: "USA",
  restaurant_count: 2
},
{
  __name__: cities/TOK,
  name: "Tokyo",
  state: null,
  country: "Japan",
  restaurant_count: 0
}

ระบุเงื่อนไขการรวมหลายรายการ

คำค้นหาต่อไปนี้จะสแกนrestaurantsกลุ่มคอลเล็กชันและทำการ รวมหลายฟิลด์กับrestaurantsกลุ่มคอลเล็กชันเพื่อค้นหาเจ้าของที่รีวิว ร้านอาหารของตนเองreviews

Node.js

let results = await execute(db.pipeline()
  .collectionGroup("restaurants")
  .define(field("owner_id"), field("__name__"))
  .where(db.pipeline()
    .collectionGroup("reviews")
    .where(field("restaurant").equal(variable("__name__")))
    .where(field("author").equal(variable("owner_id")))
    .aggregate(count().as("c"))
    .toScalarExpression()
    .greaterThan(0)));

การตอบกลับ

{
  __name__: cities/SF/restaurants/X9An0HIlx29A9GPuRthS,
  name: "Sunset Taco",
  type: "mexican",
  owner_id: "Edward"
}

การป้องกันการเข้าร่วม (NOT EXISTS)

การค้นหาต่อไปนี้จะสแกนกลุ่มคอลเล็กชัน restaurants และค้นหาร้านอาหารทั้งหมดที่ยังไม่มีรีวิว

Node.js

let results = await execute(db.pipeline()
  .collectionGroup("restaurants")
  .define(field("__name__").as("restaurant_name"))
  .where(db.pipeline()
    .collectionGroup("reviews")
    .where(field("restaurant").equal(variable("restaurant_name")))
    .aggregate(count().as("review_count"))
    .toScalarExpression()
    .equal(0)));

การตอบกลับ

{
  __name__: "cities/LA/restaurants/X9An0HIlx29A9GPuRthS",
  name: "Hollywood Sushi",
  type: "sushi",
  owner_id: "Ken Kenji"
}

Subquery as Join

คำค้นหาต่อไปนี้จะทำให้ความสัมพันธ์ระหว่างร้านพิซซ่าแต่ละร้านกับรีวิว แบนราบ การวางคําค้นหาย่อยไว้ภายในขั้นตอน unnest(...) จะทําให้เซิร์ฟเวอร์ทําซ้ำเอกสารร้านอาหารภายนอกสําหรับรีวิวที่ตรงกันแต่ละรายการ ซึ่งจะสร้างเอกสารแบบแบนที่รวมกัน (คล้ายกับ INNER JOIN ของ SQL)

Node.js

let results = await execute(db.pipeline()
  .collectionGroup("restaurants")
  .where(field("type").equal("pizza"))
  .define(field("__name__").as("restaurant_name"))
  .unnest(
    db.pipeline()
      .collectionGroup("reviews")
      .where(field("restaurant").equal(variable("restaurant_name")))
      .select("rating", "reviewer_id")
      .toArrayExpression()
      .as("review")));

การตอบกลับ

{
  __name__: "cities/SF/restaurants/xU4pu8nFpnJDPZOwcSPP",
  name: "Golden Gate Pizza",
  type: "pizza",
  owner_id: "Mario Rossi"
  review: { rating: 5, reviewer_id "Alice" }
},
{
  __name__: "cities/SF/restaurants/xU4pu8nFpnJDPZOwcSPP",
  name: "Golden Gate Pizza",
  type: "pizza",
  owner_id: "Mario Rossi",
  review: { rating: 4, reviewer_id "Bob" }
},
{
  __name__: "cities/LA/restaurants/6CYntvNgbYzgaW652Gq1",
  name: "Venice Pizza",
  type: "pizza",
  owner_id: "Luigi Romano",
  review: { rating: 3, reviewer_id "George" }
}

การสืบค้นย่อยที่ไม่สัมพันธ์กันเป็นตัวกรอง

การค้นหาต่อไปนี้ในคอลเล็กชัน reviews จะกรองโดยใช้ การค้นหาย่อยที่ไม่สัมพันธ์กับตัวมันเองเพื่อหารีวิวที่มีคะแนนมากกว่าคะแนนเฉลี่ย

Node.js

let results = await execute(db.pipeline()
  .collection("reviews")
  // Average review rating is 4.3
  .where(field("rating").greaterThan(db.pipeline()
    .collection("reviews")
    .aggregate(average("rating").as("avg"))
    .toScalarExpression())))
  .select("rating", "reviewer_id");

การตอบกลับ

{
  rating: 5,
  reviewer_id "Alice"
},
{
  rating: 5,
  reviewer_id "Diana"
},
{
  rating: 5,
  reviewer_id "Hannah"
},
{
  rating: 5,
  reviewer_id "Julia"
}

แนวทางปฏิบัติแนะนำ

  • จัดการหน่วยความจำด้วย toArrayExpression(): โปรดระมัดระวังในการใช้ toArrayExpression() คำค้นหาย่อย เนื่องจากหากสร้างเอกสารจำนวนมาก อาจทำให้เกินขีดจำกัดหน่วยความจำของคำค้นหา (128 MiB) เพื่อลดปัญหานี้ ให้ใช้ select(...) ภายในคำค้นหาย่อยเพื่อแสดงเฉพาะฟิลด์ที่จำเป็น และใช้ตัวกรอง where(...) เพื่อจำกัดจำนวนเอกสารที่แสดง พิจารณาใช้ limit(...) หากเหมาะสมเพื่อจำกัดจำนวน เอกสารที่คำค้นหาย่อยแสดง
  • การจัดทำดัชนี: ตรวจสอบว่ามีการจัดทำดัชนีฟิลด์ที่ใช้ใน where(...) ของคําสั่งย่อย การรวมที่มีประสิทธิภาพขึ้นอยู่กับความสามารถในการค้นหาดัชนีแทนการสแกนตาราง แบบเต็ม

ดูแนวทางปฏิบัติแนะนำเพิ่มเติมเกี่ยวกับคำค้นหาได้ใน คำแนะนำเกี่ยวกับการเพิ่มประสิทธิภาพคำค้นหา

ข้อจำกัด

  • subcollection(...) ขอบเขต: subcollection(...) ขั้นตอนการป้อนข้อมูลรองรับเฉพาะภายในคำค้นหาย่อย เนื่องจากต้องมีบริบทของเอกสารหลักเพื่อแก้ไขความสัมพันธ์แบบลำดับชั้นและทำการรวม
  • ระดับการซ้อน: คุณซ้อนคำสั่งย่อยได้สูงสุด 20 ระดับ
  • การใช้หน่วยความจำ: ขีดจำกัด 128 MiB สำหรับข้อมูลที่สร้างขึ้นจะมีผลกับทั้งคำค้นหา รวมถึงเอกสารที่เข้าร่วมทั้งหมด