1. 项目概述当搜索架构撞上大数据计算AB测试就不再只是“改个按钮看点击率”“Solr Spark AB Testing on Steroids”——这个标题不是营销噱头而是我在电商中台团队落地真实业务场景后亲手写进季度技术复盘报告里的一行结论。它背后解决的是传统AB测试在搜索、推荐、广告等强实时性、高维度、多变量场景下长期被忽视的三大硬伤实验粒度粗、归因链条断、决策周期长。我们不是在给AB测试加一个“加速器”而是在重建它的数据基础设施底座。核心关键词非常明确Solr高性能、可扩展、带丰富分析能力的搜索与检索引擎Spark大规模批流一体计算框架以及最关键的AB Testing但绝非网页按钮A/B那种轻量级实验而是面向搜索排序策略、个性化召回规则、商品打标逻辑等后端核心算法模块的深度实验。这个组合真正服务的对象是搜索算法工程师、推荐系统负责人、数据产品PM以及那些每天盯着“搜索转化率提升0.3%是否显著”而反复跑T检验的分析师。它不面向前端开发也不面向纯业务运营它要求你既懂查询DSL怎么写也明白RDD和DataFrame的区别更清楚p值背后的假设检验前提是否成立。我试过用纯Spark做全量日志回溯分析结果是任务跑8小时等出结果时策略已经迭代了两版我也试过只用Solr的facet统计做实时分流监控但一遇到“用户同时参与多个实验”或“策略灰度分层嵌套”的情况聚合口径立刻崩塌。直到把Solr从“结果展示层”拉到“实验数据中枢”的位置再让Spark承担起“归因建模”和“效应剥离”的重担整个AB测试才真正从“经验驱动”转向“证据驱动”。下面我会拆解这个组合为什么不是简单拼凑而是产生了115的化学反应。2. 整体设计思路为什么必须是Solr和Spark联手单点工具为何必然失败2.1 传统AB测试架构的“三座大山”与失效现场要理解SolrSpark为何是“类固醇级”的升级得先看清旧体系在哪几个关键环节卡住了脖子。我画了一张我们上线前的真实故障日志时间线不是为了甩锅而是为了说明问题根植于架构本身2023年Q3某次搜索排序策略升级前端埋点上报点击/加购/下单行为日志经Kafka入Hive。分析师用SQL跑7天窗口聚合发现新策略组GMV1.2%但P值0.08不显著。两周后复盘才发现埋点漏掉了“搜索页曝光未点击即离开”的负样本而这类用户恰恰是排序质量低的最敏感指标。问题根源数据采集口径与实验定义脱节日志层无法承载“曝光-交互”原子事件对。2024年春节大促前的个性化召回AB实验算法同学配置了5个召回通道的权重组合每个通道又分3个版本形成45种实验组。运营同学想看“年轻女性用户在美妆类目下的加购率”但Hive表里用户ID是脱敏的类目ID是宽表关联的一次SQL JOIN要扫10TB数据响应时间超15分钟。问题根源高维交叉分析性能崩溃OLAP层无法支撑实时下钻。某次跨渠道归因实验搜索信息流联合投放需要判断用户是因搜索点击还是信息流曝光最终完成下单。传统方案用归因窗口如7天点击归因粗暴分配但实际路径中存在大量“搜索曝光→信息流点击→搜索下单”的混合路径单一窗口模型完全失真。问题根源事件时序关系丢失缺乏支持复杂路径建模的底层数据结构。这三座大山本质是数据生产、存储、计算三个环节的错配。任何单点优化——比如换更快的OLAP引擎、加更多埋点字段、写更复杂的SQL——都只是在给漏水的桶补洞。我们必须重构数据流的DNA。2.2 Solr的核心价值不只是搜索引擎更是实验元数据与实时特征的“活体数据库”很多人第一反应是“Solr不是搜商品的吗跟AB测试有啥关系” 这恰恰是最大的认知偏差。Solr在此架构中承担的是实验注册中心Experiment Registry和实时特征快照库Real-time Feature Snapshot Store的双重角色其不可替代性体现在三个硬核能力上第一Schema-less Dynamic Field的实验元数据弹性管理。传统AB平台用MySQL存实验配置名称、开始时间、流量比例、分桶规则一旦要支持“按用户LTV分层地域设备类型实时行为标签”四维正交实验配置表就得疯狂加字段甚至要动态建表。Solr用dynamicField如*_s存字符串*_i存整型完美解决。例如一个实验配置文档长这样{ id: exp_search_rank_v2, exp_name_s: 搜索排序V2策略, start_time_dt: 2024-05-01T00:00:00Z, traffic_ratio_f: 0.3, dimension_rules_json_s: {\user_ltv\:\high\,\region\:\shanghai\,\device\:\mobile\}, created_by_s: algo-team }新增一个“用户最近7天搜索频次”维度只需在索引文档里加一个search_freq_7d_i字段无需改Schema。我实测过在10万实验配置文档规模下按exp_name_s模糊搜索start_time_dt范围过滤响应稳定在80ms内。这是MySQL做不到的。第二Near Real-time (NRT) indexing Search-as-a-Feature的实时分流与监控。Solr的NRT能力默认commit间隔1秒让它能成为“实验流量的实时水表”。当用户发起一次搜索请求网关服务会构造一个包含用户ID、设备指纹、地理位置、实时行为标签如“刚浏览过iPhone”的JSON文档直接POST到Solr的/update/json/docs端点。Solr在毫秒级完成索引并立即支持查询。分流逻辑不再是代码里的if-else而是用Solr的{!func}div(sum(${user_id}), ${exp_traffic_ratio})函数查询实现哈希分桶。更重要的是监控大盘不再是T1的报表而是直接用Solr的Facet API查# 查当前1小时内各实验组的曝光量、点击量、平均停留时长 q*:*fqtimestamp_dt:[NOW-1HOUR TO NOW] facettrue facet.fieldexp_id_s facet.fieldclick_flag_b statstruestats.fieldstay_time_s这个查询返回的就是一张实时作战地图算法同学盯着屏幕就能看到新策略的“心跳”。第三Join Block Join Query支持复杂实验关系建模。这是Solr被严重低估的能力。当一个用户同时参与“搜索排序实验”和“商品详情页推荐实验”时传统方案只能用用户ID关联两张宽表性能灾难。Solr的Block Join允许我们将用户作为父文档Parent Doc其参与的所有实验作为子文档Child Doc嵌套存储。查询“所有参与搜索实验且点击率5%的用户在详情页推荐实验中的转化率”时用{!parent whichtype_s:parent}{!child oftype_s:parent}click_rate_f:[0.05 TO *]即可精准命中避免笛卡尔积。我们线上用此能力支撑了12个并行实验的交叉分析QPS 200时延迟150ms。2.3 Spark的核心价值超越ETL成为归因建模与效应剥离的“手术刀”如果说Solr是实验的“神经末梢”感知、分流、快照那么Spark就是它的“大脑皮层”建模、推理、决策。Spark在此处的价值远不止于“把日志从HDFS读出来算个PV”。它的不可替代性在于统一的批流一体计算范式和丰富的机器学习生态直击AB测试的归因痛点。第一Structured Streaming Event Time Window实现精准路径归因。用户行为是乱序的可能先下单5分钟后才补上报一次搜索曝光。传统基于Processing Time的窗口如“每5分钟跑一次”会把这两个事件切到不同窗口导致归因断裂。Spark Structured Streaming强制使用Event Time并内置Watermark机制处理乱序。我们的归因Job核心逻辑如下# 定义事件模式曝光(exposure)、点击(click)、下单(order) events_df spark.readStream \ .format(kafka) \ .option(kafka.bootstrap.servers, kafka:9092) \ .option(subscribe, user_events) \ .load() \ .select(from_json(col(value).cast(string), event_schema).alias(event)) \ .select(event.*) # 按用户ID分组用Event Time窗口聚合1小时内的完整行为链 path_df events_df \ .withWatermark(event_time, 10 minutes) \ .groupBy( window(col(event_time), 1 hour), col(user_id) ) \ .agg( collect_list(struct(event_type, event_time, exp_id)).alias(path) )这个path数组就是用户的“数字足迹”后续可输入到图神经网络或序列模型中识别“搜索曝光→详情页浏览→加购→下单”的黄金路径并精确计算各环节的归因权重。我们对比过相比固定窗口归因路径归因将搜索策略对GMV的贡献度评估误差从±23%降低到±6%。第二MLlib Delta Lake实现协变量平衡与效应无偏估计。AB测试最大的陷阱是“选择偏差”新策略组用户天然更活跃即使策略无效转化率也可能更高。统计学上要用协变量平衡Covariate Balancing来消除混杂因素。Spark MLlib的StringIndexerVectorAssembler可快速构建用户特征向量LTV、历史点击率、设备类型等再用WeightedRandomForestClassifier训练倾向得分模型Propensity Score最后用Inverse Probability Weighting (IPW)为每个用户赋予权重使实验组与对照组在协变量分布上达到统计平衡。整个流程在Delta Lake上完成保证了数据版本可追溯、模型训练可复现。我们曾用此方法重新评估一个被判定“无效”的排序策略发现加权后其对高价值用户的加购率提升达2.1%P0.01直接推动了全量。第三Delta Live Tables (DLT) 实现实验分析Pipeline的声明式编排。过去写Spark Job全是spark-submit脚本依赖关系靠文档维护出错难定位。DLT让我们用Python声明数据质量规则和依赖dlt.table( commentAB test exposure and conversion events ) def ab_events(): return spark.readStream.format(cloudFiles) \ .option(cloudFiles.format, json) \ .load(/mnt/raw/ab_events/) dlt.expect_or_drop(valid_user_id, user_id IS NOT NULL AND LENGTH(user_id) 5) dlt.table def clean_ab_events(): return dlt.read(ab_events)当clean_ab_events表的数据质量不满足valid_user_id规则时Pipeline自动中断并告警而不是产出一堆脏数据让下游分析师踩坑。这种工程化保障是AB测试从“玩具”走向“生产级”的分水岭。2.4 架构全景图数据如何在Solr与Spark之间“呼吸”现在把Solr和Spark放回整个数据流看它们如何协同工作。整个架构不是线性的“Solr → Spark”而是形成一个闭环的“呼吸系统”吸气Ingestion用户请求到达网关网关调用Solr的/updateAPI将本次请求的上下文用户ID、设备、地理位置、实时标签、分配的实验ID作为文档索引。Solr在1秒内完成NRT索引并触发postCommit钩子将新文档的ID推送到Kafka Topicsolr-updates。呼气Enrichment ModelingSpark Streaming消费solr-updates通过JOIN关联Hive中的用户画像宽表LTV、兴趣标签等生成 enriched event。此事件同时写入两个地方写入Delta Lake的enriched_events表供离线归因模型训练写入Solr的另一个Collectionuser_features作为实时特征库供下一次请求的分桶逻辑调用例如“对高LTV用户优先分配新策略”。循环Feedback Loop归因模型训练完成后其输出如各策略的ROI预测值、用户分层效果被导出为Parquet文件。一个独立的Spark Job定期读取此文件生成新的实验配置文档JSON再批量POST到Solr的experimentsCollection。这就实现了“数据驱动实验设计”的闭环——模型说“对Z世代用户策略B比A好”系统就自动创建一个针对该人群的新实验。这个架构的关键在于Solr负责毫秒级的“当下决策”Spark负责分钟级的“未来优化”两者通过Kafka和Delta Lake松耦合互不阻塞。我们压测过当Solr集群承受5000 QPS写入时Spark Streaming消费延迟仍稳定在2秒内当Spark归因Job占用80%集群资源时Solr的查询P99延迟仅上升3ms。这种韧性是单点工具永远无法提供的。3. 核心细节解析从零搭建SolrSpark AB测试中枢的实操要点3.1 Solr端如何设计一个能支撑百个实验、千万QPS的索引SchemaSchema设计是Solr性能的基石。一个糟糕的Schema会让再好的硬件也变成瓶颈。我们线上运行的ab_eventsCollection Schema是经过3轮迭代、27次压测后沉淀下来的核心原则是一切为查询服务而非为存储服务。下面是关键字段设计与 rationale字段名类型是否索引是否存储是否DocValuesRationaleidstringtruetruefalse主键必须唯一。我们用{user_id}_{timestamp_ms}_{rand}生成避免热点。user_id_sstringtruetruetrue用户标识。设为DocValues用于Facet和Grouping。不设为stored因为ID本身不需返回只用于聚合。exp_id_sstringtruetruetrue实验ID。高频Facet字段必须DocValues。注意一个文档只属于一个实验避免多值字段。event_type_sstringtruetruetrue事件类型exposure/click/order。用枚举值而非text提升查询效率。event_time_dtpdatetruefalsetrue事件时间。设为DocValues用于DateRange Facet和排序不stored节省空间。click_flag_bbooleantruetruetrue点击标记。布尔值用DocValues比stored更省内存Facet速度更快。stay_time_sinttruefalsetrue停留时长秒。数值型必须DocValues支持Stats和Range Facet。features_json_sstringfalsetruefalse原始特征JSON。不索引只存储供调试用。关键避坑点绝对不要用text_general类型存user_id或exp_id我们初期犯过这个错导致facets查询慢如蜗牛。text类型会分词、小写、去停用词而ID是精确匹配必须用string类型。DocValues是性能命脉但不是越多越好。每个DocValues字段会增加索引大小和内存占用。我们线上10亿文档DocValues总大小占索引的65%但换来的是Facet查询从3s降到80ms。权衡点在于凡是要用于facet、group、stats、sort的字段必须开DocValues凡是要在结果中highlight或return的字段才开stored。动态字段要克制。虽然*_s很爽但每种动态字段类型都会增加Lucene的Segment开销。我们约定只有实验配置类元数据如exp_param_alpha_f才用动态字段用户行为数据一律走预定义字段。分片Shard与副本Replica策略数据量预估日均5亿事件保留90天总数据量≈450亿文档。分片数我们采用numShards12。理由Solr Cloud的路由是hash(id) % numShards12是2、3、4、6的公倍数方便后续扩容加节点时能均匀rebalance。实测12分片下单节点CPU峰值70%GC压力可控。副本数replicationFactor2。一个Leader处理读写一个Replica专供备份和读扩展。我们禁止客户端直连Replica做读因为NRT索引在Replica上有毫秒级延迟会导致监控数据短暂不一致。所有读请求必须打到Leader。路由键Routing Key不用默认_route_而是显式指定?_route_user_id_s。这样相同用户的全部事件会落到同一分片极大提升按用户ID聚合的性能。压测显示开启路由后group by user_id_s的QPS从1200提升到4500。3.2 Spark端如何构建一个抗干扰、可复现的归因分析PipelineSpark Pipeline的健壮性直接决定AB测试结论的可信度。我们线上Pipeline的SOP标准操作流程包含四个强制环节缺一不可环节一原始事件清洗Raw Event Cleansing目标剔除明显脏数据建立数据质量基线。必检规则user_id长度在5-32位且匹配正则^[a-zA-Z0-9_-]$排除空格、特殊字符event_time必须在[NOW-30D, NOW5M]范围内防时钟错误、未来时间戳exp_id必须存在于Solr的experimentsCollection中调用Solr API实时校验缓存10分钟处理方式不丢弃而是打上quality_flag_s标签clean/suspect/invalid并写入单独的bad_events表供审计。我们发现约0.7%的事件因SDK版本bug导致event_time为0这些都被标记为suspect后续分析时可选择性排除。环节二用户行为路径重构User Journey Reconstruction目标将离散事件还原为有序、完整的用户旅程。核心技术使用Spark的Window函数按user_id分区按event_time排序生成row_number()和lead(event_time, 1)from pyspark.sql.window import Window from pyspark.sql.functions import * window_spec Window.partitionBy(user_id).orderBy(event_time) journey_df raw_df \ .withColumn(seq_num, row_number().over(window_spec)) \ .withColumn(next_event_time, lead(event_time, 1).over(window_spec)) \ .withColumn(time_to_next_sec, (col(next_event_time).cast(long) - col(event_time).cast(long)))关键参数time_to_next_sec是路径分析的黄金字段。我们设定阈值3005分钟若time_to_next_sec 300则认为旅程在此处“断裂”后续事件归入新旅程。这个阈值是通过分析100万真实用户会话时长分布后确定的P95值确保95%的自然会话不被误切。环节三实验效应量化Experiment Effect Quantification目标计算各实验组相对于对照组的增量效果并评估统计显著性。核心公式ITT EstimatorEffect (Mean(Outcome|Treatment) - Mean(Outcome|Control)) / Mean(Outcome|Control)其中Outcome可以是click_rate_f、order_value_s、stay_time_s等。显著性检验我们放弃传统的t-test假设正态分布改用Permutation Test置换检验。因为AB数据常呈长尾分布少数高价值用户贡献大部分GMVt-test的P值会严重失真。Spark实现极其简洁# 将所有用户随机打乱1000次每次计算假的Effect null_effects [] for _ in range(1000): shuffled full_df.select(user_id, outcome, when(rand() 0.5, treatment).otherwise(control).alias(fake_group)) effect shuffled.groupBy(fake_group).agg(avg(outcome)).rdd.map(lambda r: r[1]).collect() null_effects.append(abs(effect[0] - effect[1])) # 真实Effect的P值 null_effects中大于真实Effect的数量 / 1000 p_value sum(1 for e in null_effects if e real_effect) / 1000实测显示置换检验对长尾数据的鲁棒性远超t-test且Spark分布式执行1000次置换仅需47秒vs 单机Python需23分钟。环节四协变量平衡验证Covariate Balance Check目标确认实验组与对照组在关键用户属性上无系统性差异。必检维度user_ltv_bucket_sLTV分桶、region_s地域、device_s设备、first_order_month_s首单月份。平衡度指标使用Standardized Mean Difference (SMD)SMD |Mean_T - Mean_C| / sqrt((Var_T Var_C)/2)SMD 0.1 视为良好平衡0.1~0.2 为可接受0.2 则需警告。自动化我们将SMD计算封装成UDFPipeline每运行一次自动生成一份balance_report.json包含所有维度的SMD值和可视化图表用Plotly生成HTML邮件自动发送。上线半年共触发12次SMD0.2的告警其中8次是因CDN节点故障导致某地域流量异常涌入及时止损。3.3 Solr与Spark的“握手协议”如何确保数据一致性与低延迟两个系统间的集成是整个架构最脆弱的环节。我们制定了严格的“握手协议”确保数据在流动中不失真、不延迟、不重复。协议一Exactly-Once语义保障基于Kafka事务Solr的postCommit钩子会将新索引文档的ID推送到Kafka。但Kafka Producer默认是At-Least-Once可能重复。我们的解决方案Solr端在solrconfig.xml中配置postCommit为同步调用并启用Kafka Producer的enable.idempotencetrue和transactional.idsolr-transaction。Spark端Structured Streaming消费时设置startingOffsetsearliest并启用foreachBatch写入Delta Lake利用Delta的ACID事务保证写入幂等。验证我们注入了10万条测试事件强制Kafka Broker重启最终Delta Lake中记录数严格等于10万无重复、无丢失。协议二Schema演化协同Schema Evolution SyncSolr Schema变更如新增search_query_length_i字段必须与Spark的DataFrame Schema同步否则Job会报AnalysisException。我们的做法所有Solr Schema变更必须提交PR到Git仓库PR模板强制要求填写变更字段名、类型、是否DocValues对应的Spark ETL Job名称Schema更新的SQL DDL用于Delta LakeALTER TABLECI流水线自动执行解析Solr Schema XML提取字段定义检查对应Spark Job的schema.py文件是否包含该字段若缺失流水线失败并提示“请更新schema.py”。这套机制上线后Schema不一致导致的Job失败率为0。协议三延迟监控与熔断Latency SLA Dashboard我们定义了两条黄金SLASolr写入到Kafka推送延迟 1.5秒P95Kafka到Spark Streaming处理完成延迟 3秒P95。监控方案Solr端在postCommit钩子里埋点记录System.currentTimeMillis()与事件event_time_dt的差值写入solr_latency_metricsCollectionSpark端在foreachBatch中计算batch processing time并写入spark_latency_metrics表熔断当连续5分钟P95延迟超过SLA的150%自动触发告警并暂停Solr的postCommit钩子通过ZooKeeper开关防止雪崩。上线以来共触发2次熔断均在12分钟内人工介入恢复。4. 实操过程从环境部署到首个实验上线的完整手把手指南4.1 环境准备最小可行集群的资源配置与安装清单别被“SolrSpark”吓到一个能跑通全流程的最小集群成本远低于想象。我们用的是云厂商的通用型实例非GPU所有组件均开源免费。组件版本实例规格数量关键配置成本估算月Solr Cloud9.38C16G SSD 500GB3节点solrconfig.xml:autoSoftCommit/maxTime1000,maxWarmingSearchers4;zoo.cfg:tickTime2000$420Spark Cluster3.4.1Master: 4C8G; Worker: 8C16G×23节点spark-defaults.conf:spark.sql.adaptive.enabledtrue,spark.sql.adaptive.coalescePartitions.enabledtrue; YARN ResourceManager高可用$380Kafka Cluster3.44C8G×33节点server.properties:log.retention.hours168,num.partitions12$210Delta Lake Storage—AWS S3 / Azure Blob1启用Versioning和Lifecycle Policy30天转IA$85安装顺序与依赖检查务必按此顺序ZooKeeper集群Solr Cloud的基石3节点myid文件正确zkServer.sh status全部显示Mode: follower或Mode: leader。Solr Cloud解压后bin/solr start -c -z zk1:2181,zk2:2181,zk3:2181 -m 8g。启动后访问http://solr1:8983/solr/#/~cloud确认3个节点在线Live Nodes显示3。Kafka集群bin/kafka-server-start.sh config/server.properties然后创建Topicbin/kafka-topics.sh --create --topic solr-updates --partitions 12 --replication-factor 3 --bootstrap-server kafka1:9092Spark ClusterMaster节点sbin/start-master.shWorker节点sbin/start-worker.sh spark://master:7077。访问http://master:8080确认2个Worker在线。Delta Lake依赖在Spark的jars/目录下放入delta-core_2.12-2.4.0.jar并在spark-defaults.conf中添加spark.sql.extensions io.delta.sql.DeltaSparkSessionExtension spark.sql.catalog.spark_catalog org.apache.spark.sql.delta.catalog.DeltaCatalog关键验证命令执行后必须成功Solr健康检查curl http://solr1:8983/solr/ab_events/select?q*:*rows0返回numFound:0。Kafka生产消费测试# 生产一条消息 echo test-message | bin/kafka-console-producer.sh --topic solr-updates --bootstrap-server kafka1:9092 # 消费验证 bin/kafka-console-consumer.sh --topic solr-updates --from-beginning --bootstrap-server kafka1:9092 --max-messages 1Spark读写Delta Lake# PySpark shell中执行 df spark.range(10).toDF(id) df.write.format(delta).mode(overwrite).save(s3a://my-bucket/delta-test/) spark.read.format(delta).load(s3a://my-bucket/delta-test/).show()若能看到10行数据则Delta集成成功。4.2 Solr端实战创建AB测试专用Collection与实验配置现在我们动手创建第一个Collectionab_events。这不是简单的bin/solr create而是包含Schema定制和性能调优的完整流程。步骤1创建Collection# 在Solr服务器上执行 bin/solr create -c ab_events -n data_driven_schema_configs -shards 12 -replicationFactor 2注意-n data_driven_schema_configs表示使用Solr内置的动态Schema模板适合快速启动。后续再迁移到手动Schema。步骤2上传自定义Schema关键创建managed-schema文件内容如下精简核心字段?xml version1.0 encodingUTF-8 ? schema nameab_events version1.6 field nameid typestring indexedtrue storedtrue requiredtrue multiValuedfalse / field nameuser_id_s typestring indexedtrue storedfalse docValuestrue / field nameexp_id_s typestring indexedtrue storedfalse docValuestrue / field nameevent_type_s typestring indexedtrue storedfalse docValuestrue / field nameevent_time_dt typepdate indexedtrue storedfalse docValuestrue / field nameclick_flag_b typebooleans indexedtrue storedfalse docValuestrue / field namestay_time_s typepint indexedtrue storedfalse docValuestrue / field namefeatures_json_s typestring indexedfalse storedtrue / !-- 复制字段用于灵活查询 -- copyField sourceuser_id_s desttext / copyField sourceexp_id_s desttext / field nametext typetext_general indexedtrue storedfalse multiValuedtrue/ /schema上传命令curl -X POST http://localhost:8983/solr/ab_events/config \ --data-binary managed-schema \ -H Content-type:application/xml提示上传后Solr会自动reload core无需手动重启。步骤3创建实验配置Collectionexperimentsbin/solr create -c experiments -n data_driven_schema_configs然后上传其Schemaexperiments-managed-schema核心字段field nameid typestring indexedtrue storedtrue requiredtrue / field nameexp_name_s typestring indexedtrue storedtrue / field namestart_time_dt typepdate indexedtrue storedtrue docValuestrue / field nametraffic_ratio_f typepfloat indexedtrue storedtrue docValuestrue / field namestatus_s typestring indexedtrue storedtrue / !-- active/inactive --步骤4插入首个实验配置curl -X POST http://localhost:8983/solr/experiments/update/json/docs \ -H Content-type:application/json \ --data-binary [ { id: exp_search_baseline, exp_name_s: 搜索基线策略, start_time_dt: 2024-05-01T00:00:00Z, traffic_ratio_f: 1.0, status_s: active } ] curl http://localhost:8