从旅客完整旅程中学习:如何构建更懂用户的 Airbnb 搜索个性化

2026-07-22 22 预计阅读时间: 1 分钟
来源: medium.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

住宿搜索的难点不只是找出“质量最高”的房源,而是判断某位旅客在当前阶段最可能需要什么。来源没有提供技术摘要,因此无法确认 Airbnb 实际采用的模型、特征或系统架构。仅从标题可以确定一个重要方向:个性化搜索不应只观察当前查询,还要从旅客跨越发现、比较、收藏、预订与入住的完整旅程中学习。

为什么单次查询不够

同一句“东京住宿”,在不同旅程阶段可能表达完全不同的需求:

  • 刚开始规划时,用户可能在比较区域、价格和房型,日期尚未确定。
  • 已收藏多个新宿房源后,用户再次搜索东京,区域偏好已经相当明确。
  • 临近出发时,用户可能更关心即时确认、入住时间和交通距离。
  • 完成一次家庭旅行后,下一次搜索仍可能延续对整套房源、厨房和多床位的偏好。

传统排序通常以查询、房源质量和总体转化率为中心。旅程驱动的个性化则增加了一层上下文:当前请求属于哪个阶段,哪些历史行为仍然有效,以及本次搜索与过去行程是否相关。

这意味着系统需要区分三类信号:

  1. 短期意图:最近查看的城市、日期、价格区间和房型,通常对当前会话影响最大。
  2. 稳定偏好:长期反复出现的整套房源、卧室数量或设施偏好,但不能把偶发行为误认为永久标签。
  3. 旅程状态:探索、收敛、已预订或行程结束。状态不同,同一行为的解释也不同。

把旅程转换成排序特征

可以将旅客行为表示为带时间戳的事件流,例如 searchviewsavebooktrip_completed。在线排序服务不必读取完整历史,而应由特征流水线提前生成紧凑状态:

  • 最近目的地及其时间衰减权重;
  • 最近浏览和收藏房源的价格中位数;
  • 偏好的房型、区域和设施分布;
  • 当前旅程阶段;
  • 当前候选房源与历史偏好的匹配程度。

一个可解释的初始排序公式可以写成:

final_score = 0.55 * relevance
            + 0.20 * listing_quality
            + 0.15 * journey_match
            + 0.10 * preference_match

这些权重只是可以这样实践的起点,并不代表 Airbnb 的实现。生产系统通常还要加入可订状态、价格、地理距离、业务约束和探索机制。尤其要避免让个性化分数压过硬约束:没有可用日期的房源,无论多符合偏好都不应排在可预订房源之前。

一个可运行的旅程感知排序原型

下面的 Python 示例只使用标准库,可以直接保存为 journey_ranker.py 后运行。它演示如何对历史事件做时间衰减,再把用户近期表现出的区域与房型偏好加入基础相关性分数。

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from math import exp


@dataclass(frozen=True)
class Event:
    kind: str
    area: str
    room_type: str
    occurred_at: datetime


@dataclass(frozen=True)
class Listing:
    id: str
    area: str
    room_type: str
    relevance: float
    quality: float


def decay(age_days: float, half_life_days: float = 14.0) -> float:
    return exp(-0.693 * age_days / half_life_days)


def build_preferences(events: list[Event], now: datetime):
    area_scores: dict[str, float] = {}
    room_scores: dict[str, float] = {}
    action_weight = {"view": 1.0, "save": 2.5, "book": 4.0}

    for event in events:
        age = max(0.0, (now - event.occurred_at).total_seconds() / 86400)
        weight = action_weight.get(event.kind, 0.5) * decay(age)
        area_scores[event.area] = area_scores.get(event.area, 0.0) + weight
        room_scores[event.room_type] = room_scores.get(event.room_type, 0.0) + weight

    return area_scores, room_scores


def normalize_match(value: float, scores: dict[str, float]) -> float:
    maximum = max(scores.values(), default=0.0)
    return 0.0 if maximum == 0 else scores.get(value, 0.0) / maximum


def rank(listings: list[Listing], events: list[Event], now: datetime):
    area_scores, room_scores = build_preferences(events, now)
    ranked = []

    for listing in listings:
        area_match = normalize_match(listing.area, area_scores)
        room_match = normalize_match(listing.room_type, room_scores)
        preference_match = 0.6 * area_match + 0.4 * room_match
        score = (
            0.60 * listing.relevance
            + 0.25 * listing.quality
            + 0.15 * preference_match
        )
        ranked.append((round(score, 4), listing))

    return sorted(ranked, key=lambda item: item[0], reverse=True)


if __name__ == "__main__":
    now = datetime.now(timezone.utc)
    events = [
        Event("view", "Shinjuku", "entire_home", now - timedelta(days=7)),
        Event("save", "Shinjuku", "entire_home", now - timedelta(days=2)),
        Event("view", "Asakusa", "private_room", now - timedelta(days=1)),
    ]
    listings = [
        Listing("A", "Shinjuku", "entire_home", 0.78, 0.90),
        Listing("B", "Asakusa", "private_room", 0.84, 0.88),
        Listing("C", "Shibuya", "hotel_room", 0.91, 0.92),
    ]

    for score, listing in rank(listings, events, now):
        print(f"{listing.id}: score={score}, area={listing.area}")

运行命令:

python journey_ranker.py

这个原型适合验证特征定义和排序直觉,但不能直接承担生产流量。实际系统需要处理事件乱序、跨设备身份、特征延迟、冷启动和毫秒级在线服务,并为每个特征维护版本与生成时间。

评估不能只盯着点击率

旅程学习很容易制造短期指标提升:模型把用户看过的相似房源不断推到前面,点击率可能上升,但结果会越来越单一。更可靠的评估应覆盖完整漏斗:

  • 搜索结果点击率和收藏率;
  • 从搜索到预订的转化率与所需时间;
  • 取消率、客服问题和入住后满意度;
  • 结果多样性、新房源曝光与价格覆盖;
  • 新用户、低频用户及不同地区用户的分组表现。

离线评估还要防止标签泄漏。例如,使用预订之后产生的事件去预测该次预订,会得到虚高结果。训练样本必须按照事件时间截断,确保模型只能看到排序发生之前可用的信息。

隐私边界同样不可后补。系统应限制事件保留期限,对敏感属性和精确位置保持克制,提供关闭个性化或清除历史的能力,并记录每个在线特征的用途。跨旅程使用偏好时,还应设置衰减与重置机制,避免一次商务出行永久改变家庭度假的搜索结果。

落地时从可解释的小闭环开始

采用旅程信号时,可以按以下顺序推进:

  • 先定义旅程边界和事件语义,解决重复、缺失与乱序问题。
  • 从近期目的地、收藏区域和房型等少量可解释特征开始。
  • 保留无个性化基线,并通过 A/B 测试检查完整预订漏斗。
  • 为新用户设置可靠的热门度与相关性回退策略。
  • 监控多样性、取消率、特征新鲜度和不同用户群体的效果。
  • 给历史偏好设置时间衰减、行程结束重置和用户控制入口。

旅程驱动搜索的价值,不在于无限积累用户数据,而在于正确理解行为发生的上下文。一个收藏动作究竟代表长期偏好、当前行程中的比较,还是已经失效的计划,往往比动作本身更重要。系统只有同时处理时间、阶段和边界,个性化才会真正帮助旅客缩短决策过程,而不是把过去不断复制到下一次搜索中。


相关推荐