倒排索引在规则匹配上的应用

业务背景

在日常生产的过程中,相信大家也一定遇到过这样的场景,在创建一个活动或规则时,需要指定不同的维度信息;在活动或规则匹配时,同时满足设置的维度信息,我们才认为这个活动或规则被选取到。

示例

创建一个活动,活动中定义四个规则,有大量的订单,进行活动匹配,查看订单是否满足活动定义的规则,注意活动不是一个,订单也不是一个。

如何判断订单是否匹配该活动?

在判断一个订单中推广员是否匹配这个规则时,我们先通过请求的推广内容、推广渠道、点击平台、下单平台来获取所有符合条件的推广员等级规则,然后再通过时间段、推广员id等信息进行过滤,最后获取到它的等级。

举例: 规则名称z:定向指定中等级站长

时间段: 2022-10-01 ~ 2022-12-01 站长id: 52267 计佣等级: 中等级 cookie有效期: 无 推广内容: 单品、店铺 推广渠道: 站长端 点击平台: 微信、QQ 下单平台: 极速版APP

说明: 各个维度中括号里的数字表示一个枚举值,用于存储

可以预知,在维度信息判定时,请求的订单必须满足(单品||店铺)&& 站长端 &&(微信||QQ)&& 极速版APP 这样的条件,才能初步选取到我们设置的这个规则。

那假如我们的维度信息是如下这样: 推广内容: 不限 推广渠道: 站长端 点击平台: 微信、QQ

那我们在判断的时候,订单仅满足 站长端&&(微信||QQ)&&极速版APP 这样的条件就能初步选取到这个规则了,不需要关心推广内容是什么。

接下来的问题是: 上述这样的规则或活动信息如何存储?

将活动信息和维度信息分两张表存储,还以上面的例子为例。 站长等级表 user_level_rule(id,rule_name,start_date,end_date,union_ids,union_level,cookie_expire)

站长等级维度表 user_level_rule_dimension(id,rule_id,type,type_value) 其中维度表的rule_id就是 站长等级表的id

type枚举: 推广内容 1推广渠道 2 点击平台 3 下单平台 4

type_value 枚举: 推广内容(单品1、店铺2)、推广渠道(站长端1)、点击平台(微信1、QQ2)、下单平台(极速版app1)

按上述的例子,会生成如下记录:

user_level_rule (100, '定向指定中等级推广员', 2033100, 2033100, '52267', 3, '');

user_level_rule_dimension(1, 100, 1, 1);

user_level_rule_dimension(2, 100, 1, 2);

user_level_rule_dimension(3, 100, 2, 1);

user_level_rule_dimension(4, 100, 3, 1);

user_level_rule_dimension(5, 100, 3, 2);

user_level_rule_dimension(6, 100, 4, 1);

具体该如何匹配? 匹配的难点在于,根据请求的信息获取到所有符合维度的rule_id,难在符合维度的判断,如请求仅有3个围堵,而配置了4个维度,这个rule_id就不该被选取到,另外如果请求3个维度,也配置了3个围堵,但是配置的维度值不包含请求的维度值,也不该被选取到。大家想想,上述我仅仅是举例了一个规则,但在实际生产中,会配置很多这样的规则。

如何匹配的问题本质是维度匹配的问题,如何判断请求维度信息包含在配置的维度中。

一、流量极小的场景,查询db

一个笨方法,现根据unionId和下单时间,从user_level_rule表先获取到rule_id,然后逐条遍历rule_id,判断rule_id对应的维度是否包含请求的维度。

二、流量很大的场景,db无法承压

实际生产中,我们设置这样的活动或规则,一般都是面向C端或者是流量的,不论是从性能还是db的负载来说,上述的办法都没法满足我们的要求。

从活动、规则设置的场景来说,一般都是T+1生效。那我们可以使用构建缓存的方式来解决db承压的问题。如下: 将数据库中的规则信息构建如下Map Map<Integer,UserLevelRuleInfo> userLevelRuleInfoMap;

将数据库中的维度信息构建两个Map,如下: Map<Integer,Map<Integer,Set<Integer>>> userLevelPositiveIndexMap; ruleId -> type -> List<typeValue> - ruleId到维度及维度值的索引

Map<String,List<Integer>> userLevelNegativeIndexMap type_typeValue -> List<ruleId> - 是维度_维度值到ruleId的索引

先根据请求的单个维度信息从userLevelNegativeIndexMap索引获取List<ruleId> ruleIdList 遍历ruleIdList,构建一个请求的Map<Integer,Map<Integer,Set<Integer>>> userLevelReqMap 当所有请求的维度都遍历完成时,userLeveReqMap也就构建完成了

接下来,将userLevelReqMap和userLevelPositiveIndexMap进行对比,遍历userLeveReqMap,判断ruleId请求的维度和索引的维度是否匹配。

遍历完成后,我们就选取到了符合条件的ruleId,然后再通过ruleId到userLevelRuleInfoMap,获取到UserLevelRuleInfo, 然后过滤时间和unionId即可,最终获取到了站长等级。

这个方案有什么弊端?

假如我配置的规则数量是千万级的,通过单个维度可以找出上万条、上十万条、上百万条记录,那所有维度构建完的userLievelReqMap是不是也是千万级的,然后再遍历和索引userLevelRuleInfoMap对比,性能可想而知。

三、流量很大的场景,性能要求很高

通过上述的方案,我们把问题可以归结为: 如何快速的判断请求的维度及维度值是否包含在我们预设置好的维度中

那我们就可以常识使用Java中的BitSet来解决这个问题了,

BitSet构造方法:

BitSet();//创建一个默认的BitSet对象,BitSet中数组大小会随着需要增加 BitSet(int size);// 指定初始数组大小,所有位初始化为0 void set(int index);//将指定索引处的位置设置为true

//示例

BitSet bitSet = new BitSet();

bitSet.set(1);

System.out.println(bitSet);

System.out.println(bitSet.get(1)); 第一个构造方法创建一个默认的对象,BitSet中数组大小会随需要增加

//输出

{1}

true其他操作//返回此 BitSet 中设置为 true 的位数

int cardinality()

//对此目标位 set 和参数位 set 执行逻辑与操作

void and(BitSet set)

//对此位 set 和位 set 参数执行逻辑或操作

void or(BitSet bitSet)

//将指定的 fromIndex(包括)到指定的 toIndex(不包括)范围内的每个位设置值翻转

void flip(int startIndex, int endIndex)

//示例

BitSet bitSet = new BitSet();

bitSet.set(1);

bitSet.set(2);

bitSet.set(3);

System.out.println("bitSet.cardinality:" + bitSet.cardinality());

BitSet bitSet2 = new BitSet();

bitSet2.set(3);

bitSet2.set(4);

bitSet.and(bitSet2);

System.out.println("bitSet and BitSet2:" + bitSet);

BitSet bitSet3 = new BitSet();

bitSet3.set(1);

bitSet3.set(2);

bitSet3.set(3);

BitSet bitSet4 = new BitSet();

bitSet4.set(3);

bitSet4.set(4);

bitSet3.or(bitSet4);

System.out.println("bitSet3 or BitSet4:" + bitSet3);

BitSet bitSet5 = new BitSet();

bitSet5.set(0);

System.out.println("before flip:" + bitSet5);

bitSet5.flip(0,10);

System.out.println("after flip:" + bitSet5);

//输出

bitSet.cardinality:3

bitSet and BitSet2:{3}

bitSet3 or BitSet4:{1, 2, 3, 4}

before flip:{0}

before flip:{0}

after flip:{1, 2, 3, 4, 5, 6, 7, 8, 9}可以看到,BitSet在set时,会将对应索引位设置为true,多个BitSet之间可以通过位运算取交或取并。下面我们看看如何通过BitSet来加速查询。BitSetIndex我们构建这样的一个结构

typescript 复制代码
public class BitSetIndex {

    // directType --> directValue --> bitset
    private Map<String, Map<String, BitSet>> directIndex;

    // ruleId list
    private List<Object> ruleIds;
}

其中directIndex存储了 维度类型 -> 维度值 -> BitSet索引值的关系。ruleIds存储了规则id。那他们是如何关联起来的呢?

通过BitSet的bitIndex和List中的下标关联起来的,bitIndex == index -> ruleId,在维护这个结构的时候,需要特别注意这个ruleIds的写入顺序,我们返回来看这个Map<String, Map<String,BitSet>> directIndex, 这个数据如何构建,以及通过请求如何查询到这个索引? 构建directIndex还以上篇文章的例子为例,

假设我们设置的规则及维度数据如下: type枚举:推广内容 1 推广渠道 2 点击平台 3 下单平台 4 type_value枚举:推广内容(单品 1、店铺 2)、推广渠道(站长端 1)、点击平台(微信 1、QQ 2)、下单平台(极速版app 1) user_level_rule (100, '定向指定中等级站长', 20221001, 20221201, '52267', 3, ''); user_level_rule_dimension(1, 100, 1, 1);user_level_rule_dimension(2, 100, 1, 2);user_level_rule_dimension(3, 100, 2, 1);user_level_rule_dimension(4, 100, 3, 1);user_level_rule_dimension(5, 100, 3, 2);user_level_rule_dimension(6, 100, 4, 1);

我们这里仅对维度信息执行构建,可以看到,需要维护一个ruleId和索引之间的关系,因为BitSet中存储的是一个下标。 现将上述维度列表转换为: Map<String, Map<String, List<Object>>> directionMap,type->typeValue->List<ruleId>统计我们需要构建的ruleId列表,生成ruleIds下面就是根据directionMap和ruleIds生产Map<String, Map<String, BitSet>> directIndex

ini 复制代码
private Map<String, Map<String, BitSet>> makeIndex(Map<String, Map<String, List<Object>>> directionMap, List<Object> ruleIds) {

if (directionMap == null) {
    return null;
}
int size = ruleIds.size();

// ruleIds和下标的对应关系Map
Map<Object, Integer> castIndexMap = new HashMap<>(size);
for (int i = 0; i < size; i++) {
    castIndexMap.put(ruleIds.get(i), i);
}

Map<String, Map<String, BitSet>> result = new HashMap<>(directionMap.size());
for (Map.Entry<String, Map<String, List<Object>>> m : directionMap.entrySet()) {
    String key = m.getKey(); // 定向维度
    Map<String, List<Object>> values = m.getValue();

    Map<String, BitSet> rs = result.computeIfAbsent(key, k -> new HashMap<>(2));

    for (Entry<String, List<Object>> dc : values.entrySet()) {
        String k = dc.getKey(); // 定向维度值
        List<Object> value = dc.getValue(); // ruleIds

        BitSet b = rs.get(k);
        if (b == null) {
            b = new BitSet(size);
            rs.put(k, b);
        }
        for (Object cid : value) {
            b.set(castIndexMap.get(cid));
        }
    }
}
return result;
}

上述例子经上述转换后,会生成如下directIndex

可以看到各位维度都指向了索引为0的BitSet。索引为0的ruleIds的值为100,也就是我们设置的规则id,请求维度匹配,假如我有有如下请求维度:type 1 typeValue 1type 2 typeValue 1type 3 typeValue 1type 4 typeValue 1将上述维度聚合为:Map<String, List> conditionMap 这样的结构。聚合后如下:

请求维度conditionMap和索引directIndex进行匹配

ini 复制代码
    private static BitSet getRuleIdIndex(Map<String, String[]> conditionMap, Map<String, Map<String, BitSet>> directIndex) {
List<BitSet> bs = new ArrayList<BitSet>();
for (Map.Entry<String, String[]> entry : conditionMap.entrySet()) {
    String directType = entry.getKey();
    String[] directValue = entry.getValue();

    Map<String, BitSet> map = directIndex.get(directType);
    if (map == null) {// 数据库定向条件中无此维度的,可以跳过
        continue;
    }
    BitSet b = getBitSet(map, directValue);
    if (b == null) {// 如果在当前(数据库的)定向维度值中找不到投放,那么直接返回找不到~
        return null;
    } else {
        bs.add(b);
    }
}

if (bs.isEmpty()) {
    return null;
}
BitSet bitSet = bs.get(0);
Object bitSetClone = bitSet.clone();
if (bitSetClone instanceof BitSet) {
    BitSet res = (BitSet) bitSetClone;
    for (BitSet b : bs) {
        res.and(b);
    }
    return res;
}
return null;
    }

通过上述的操作,我们就找到了所有符合维度的BitSet,遍历这个BitSet,我们就能找到所有的RuleId,万里长征的最难的一步已经跨过去了~反过头来思考,这个查找之所以快,就是因为使用了BitSet结构,一个是它存储的仅仅是一个下标以及对应的布尔值,另外就是使用了位运算,性能的提升就是在这里体现的。 假如我们有如下请求维度:type 1 typeValue 1type 2 typeValue 1type 3 typeValue 1在请求维度conditionMap和索引directIndex进行匹配时,是不是也能找到bitIndex为0的计划,也即ruleId为100。但事实上我们在设置这个规则时,还指定了 type=4 typeValue=1的维度,不应该找到这个规则才对。归结起来,就是下面两个问题: 1.设置时设置的维度值少,如仅设置了type为1、2、3的值,而未设置4,因为请求维度的不确定性,索引该如何创建,如何标识未设置的维度? 2.那又该如何匹配?上述匹配的循环中,遍历的可以请求的conditionMap

上述两个问题归结起来,就是请求的维度和设置的维度不确定,但是这里已知的一个信息是所有的维度,那如果我们在构建索引时,将设置维度时未设置的维度类型设置为固定值"不限",在构建请求的conditionMap时,我们将请求里不包括在所有维度里的维度类型设置为固定值"不限",是不是就能正常匹配了~

下面我们来看示例。示例还以之前的例子为例~type枚举:推广内容 1 推广渠道 2 点击平台 3 下单平台 4type_value枚举:推广内容(单品 1、店铺 2)、推广渠道(站长端 1)、点击平台(微信 1、QQ 2)、下单平台(极速版app 1)user_level_rule (100, '定向指定中等级站长', 20221001, 20221201, '52267', 3, '');user_level_rule_dimension(1, 100, 1, 1);user_level_rule_dimension(2, 100, 1, 2);user_level_rule_dimension(3, 100, 2, 1);user_level_rule_dimension(4, 100, 3, 1);user_level_rule_dimension(5, 100, 3, 2);可以看到共计4个维度,我们设置时指定了3个(推广内容 1 推广渠道 2 点击平台 3),那我们在构建directIndex时,维度4设置一个固定值"other",会生成如下directIndex:

假如我们有如下请求维度:type 1 typeValue 1type 2 typeValue 1同样,对于维度3、维度4我们设置一个固定值 "other",会生成如下的conditionMap:

这样在匹配的时候,无论请求还是索引都包含了所有维度,在匹配的时候就不会出现上述的问题了~ 代码实现索引维度补充

ini 复制代码
    otherprivate void init(List<Direction<T>> directionList, String[] directions) {
   
BitSetMaker maker = new BitSetMaker();

List<String> noLimitTypeList = new ArrayList<>(Arrays.asList(directions));


List<T> idList = new ArrayList<>();
for (Direction<T> d : directionList) {
    maker.addNode(d.getType().trim(), d.getValue().trim(), d.getId());
    if(!idList.contains(d.getId())){
        idList.add(d.getId());
    }
    noLimitTypeList.remove(d.getType());
}
this.ids = idList;

List<Object> idObjects = new ArrayList<>(ids);

//处理某个维度没有规则的情况
if (CollectionUtils.isNotEmpty(noLimitTypeList)) {
    for (String type : noLimitTypeList) {
        for (T t : ids) {
            maker.addNode(type, Dict.NO_LIMIR_CODE, t);
        }
    }
}
this.bitSetIndex = maker.createIndex(idObjects);
}

其中 Dict.NO_LIMIR_CODE 就是 "other"。 请求维度补充 ``` otherprivate static void preTreatConditionMap(Map<String, String[]> conditionMap, String[] allDirections) { if (allDirections == null || allDirections.length == 0) { return; } for (String d : allDirections) { if (conditionMap.get(d) == null || conditionMap.get(d).length == 0) { conditionMap.put(d, new String[] {"other"}); } } }

ini 复制代码
存在的隐患大家仔细看看索引维度补充other的代码,其实还是存在问题,下面我给大家举例说明:user_level_rule (100, '定向指定中等级站长', 20221001, 20221201, '52267', 3, '');user_level_rule_dimension(1, 100, 1, 1);user_level_rule_dimension(2, 100, 2, 1);user_level_rule (200, '定向指定高等级站长', 20221001, 20221031, '52268', 3, '');user_level_rule_dimension(3, 200, 3, 1);user_level_rule_dimension(4, 200, 4, 2);我们创建了2个规则,这两个规则覆盖了所有的维度~ 进而生成的 directionList 就包含了上述的4条维度数据~ 执行init方法时,遍历完 directionList, noLimitTypeList为空,那此时 ruleId为100的规则,维度3和维度4其实没有补充上,同理 ruleId为200的规则也有同样的问题~其实init方法这里补充"other",是针对所有入参的规则都没有某个维度时,才会进行补充"other"~解决办法上篇中,我们在构建 directIndex 时,使用了如下实现:
    ```
    private Map<String, Map<String, BitSet>> makeIndex(Map<String, Map<String, List<Object>>> directionMap, List<Object> ruleIds) {
if (directionMap == null) {
    return null;
}
int size = ruleIds.size();

// ruleIds和下标的对应关系Map
Map<Object, Integer> castIndexMap = new HashMap<>(size);
for (int i = 0; i < size; i++) {
    castIndexMap.put(ruleIds.get(i), i);
}

Map<String, Map<String, BitSet>> result = new HashMap<>(directionMap.size());
for (Map.Entry<String, Map<String, List<Object>>> m : directionMap.entrySet()) {
    String key = m.getKey(); // 定向维度
    Map<String, List<Object>> values = m.getValue();

    Map<String, BitSet> rs = result.computeIfAbsent(key, k -> new HashMap<>(2));

    for (Entry<String, List<Object>> dc : values.entrySet()) {
        String k = dc.getKey(); // 定向维度值
        List<Object> value = dc.getValue(); // ruleIds

        BitSet b = rs.get(k);
        if (b == null) {
            b = new BitSet(size);
            rs.put(k, b);
        }
        for (Object cid : value) {
            b.set(castIndexMap.get(cid));
        }
    }
}
return result;
    }

思路还是针对这个方法进行升级,升级后如下:

ini 复制代码
 private Map<String, Map<String, BitSet>> makeIndex(Map<String, Map<String, List<Object>>> directionMap, Map<Object, Set<String>> castDirectMap, List<Object> ruleIds) {
if (directionMap == null) {
 return null;
}
int size = ruleIds.size();

// ruleIds和下标的对应关系Map
Map<Object, Integer> castIndexMap = new HashMap<>(size);
for (int i = 0; i < size; i++) {
 castIndexMap.put(ruleIds.get(i), i);
}

Map<String, Map<String, BitSet>> result = new HashMap<>(directionMap.size());
for (Map.Entry<String, Map<String, List<Object>>> m : directionMap.entrySet()) {
 String key = m.getKey(); // 定向维度
 Map<String, List<Object>> values = m.getValue();

 Map<String, BitSet> rs = result.computeIfAbsent(key, k -> new HashMap<>(2));

 for (Entry<String, List<Object>> dc : values.entrySet()) {
     String k = dc.getKey(); // 定向维度值
     List<Object> value = dc.getValue(); // ruleIds

     BitSet b = rs.get(k);
     if (b == null) {
         b = new BitSet(size);
         rs.put(k, b);
     }
     for (Object cid : value) {
         b.set(castIndexMap.get(cid));
     }
 }

 // 处理当前这个维度通投的情况
 BitSet other = new BitSet(size);
 boolean hasOther = false;
 for (Object castId : ruleIds) {
     Set<String> ds = castDirectMap.get(castId);
     if (ds == null || !ds.contains(key)) {
         hasOther = true;
         other.set(castIndexMap.get(castId));
     }
 }
 if (hasOther) {
     rs.put(Dict.NO_LIMIR_CODE, other);
 }
}
return result;
 }

castDirectMap 是 ruleId 和他已有维度的一个对应关系, 我们通过设置某个维度为固定值解决了匹配错误的问题

新需求,在设置规则的时候,要求某些维度不包含某些指定值。

还以之前的例子为例:

type枚举:推广内容 1 推广渠道 2 点击平台 3 下单平台 4

type_value枚举:推广内容(单品 1、店铺 2)、推广渠道(站长端 1)、点击平台(微信 1、QQ 2)、下单平台(极速版app 1) 我们在创建规则时,指定该规则推广内容不包含 店铺。

user_level_rule (300, '定向指定中等级站长', 20221001, 20221201, '52267', 3, '');

user_level_rule_dimension(1, 100, 1, 1, 1);

user_level_rule_dimension(2, 100, 1, 2, 0);

user_level_rule_dimension(3, 100, 2, 1, 1);

维度表最后一个值是字段 isPositive,1表示包含 0表示不包含。

下面看看该如何解决不包含的问题~

思路

如果请求的任意一个维度及维度值,在我们设置的规则不包含维度范围内,那我们就认为这个请求没有匹配上这个规则 我们规定维度包含某个值定义为正定向、维度不包含某个值定义为反定向,分别用1和0表示。

在构建索引时,我们构建两个directIndex,对于正定向的构建还和之前一样,对于反定向的构建与正定向类似,只不过不需要处理没设置的维度(即不用针对未设置的维度设置"other"值),对于反定向来说,只要匹配中一个维度我们就认为请求不匹配这个规则。

将反定向匹配单独拿出来说,底层借助的其实是BitSet#or() 操作,只要任一维度匹配,我们就将对应BitIndex位选出来。

但是现实是规则中既有包含的维度、也有不包含的维度,该如何处理呢?

假设我们BitSetIndex 中ruleIds的大小为10 先通过正定向获取到bitIndex为{3,4,8}的 BitSet

假设一: 再通过反定向获取到bitIndex为{1}的BitSet,该如何和正定向的结果{3,4,8}取交,先说结果,我们应该选取到最终的bitIndex为{3,4,8}

假设二:再通过反定向获取到bitIndex为{3}的BitSet,该如何和正定向的结果{3,4,8}取交,先说结果,我们应该选取到最终的bitIndex为{4,8}

在上面,我们提到了bitSet有如下的api: // 将指定的fromIndex(包含) 到指定的toIndex(不包含)范围内的每个位设置值翻转 void flip(int startIndex, int endIndex) 若我们将反定向的bitIndex进行flip(0, ruleIds.size()),上面的两个假设分别会得到如下结果

假设一: 反定向bitIndex会变为:{0,2,3,4,5,6,7,8,9} 假设二: 反定向bitIndex会变为:{{0, 1, 2, 4, 5, 6, 7, 8, 9} 然后再和正定向的结果{3,4,8}取交,是不是就得到了我们想要的结果~

升级

因为涉及到正定向和反定向的两个directIndex,所以我们在上述的Map<String, Map<String, BitSet>> directIndex 外再加一层Map,"+"号表示正定向,"-"号表示反定向,结果如下:

Map<String, Map<String, Map<String, BitSet>>> directIndex : +|- --> directType --> directValue --> bitset

构建索引的逻辑进行升级:

ini 复制代码
  private Map<String, Map<String, BitSet>> makeIndex2(
  Map<String, Map<String, List<Object>>> directionMap,
  Map<Object, Set<String>> castDirectMap, List<Object> castIds, int isPositive) {
if (directionMap == null) {
  return null;
}
int size = castIds.size();

// castId和下标的对应关系Map
Map<Object, Integer> castIndexMap = new HashMap<Object, Integer>(size);
for (int i = 0; i < size; i++) {
  castIndexMap.put(castIds.get(i), i);
}

Map<String, Map<String, BitSet>> result = new HashMap<String, Map<String, BitSet>>(directionMap.size());
for (Entry<String, Map<String, List<Object>>> m : directionMap.entrySet()) {
  // 处理常规定向
  String key = m.getKey(); // 定向维度
  Map<String, List<Object>> values = m.getValue();

  Map<String, BitSet> rs = result.computeIfAbsent(key, k -> new HashMap<>(2));

  for (Entry<String, List<Object>> dc : values.entrySet()) {
      String k = dc.getKey(); // 定向维度值
      List<Object> value = dc.getValue(); // 投放单ID列表

      BitSet b = rs.get(k);
      if (b == null) {
          b = new BitSet(size);
          rs.put(k, b);
      }
      for (Object cid : value) {
          b.set(castIndexMap.get(cid));
      }
  }
  // 反定向不需要处理通投的情况
  if (isPositive == 1) {
      // 处理当前这个维度通投的情况
      BitSet other = new BitSet(size);
      boolean hasOther = false;
      for (Object castId : castIds) {
          Set<String> ds = castDirectMap.get(castId);
          if (ds == null || !ds.contains(key)) {
              hasOther = true;
              other.set(castIndexMap.get(castId));
          }
      }
      if (hasOther) {
          rs.put("other", other);
      }
  }
}
return result;
  }

匹配的逻辑进行升级:

ini 复制代码
    public static List<Object> doQuery(Map<String, String[]> conditionMap, BitSetIndex bitSetIndex, String[] allDirections) {
preTreatConditionMap(conditionMap, allDirections);
// 取正定向结果
Map<String, Map<String, BitSet>> posInx = bitSetIndex.getDescIndex().get("+");
int size = bitSetIndex.getCastIds().size();
BitSet poi = null;
if (posInx != null) {
    poi = getPositiveResult(conditionMap, posInx);
}
if (poi == null) {// 正定向可能为空
    poi = new BitSet(size);
}

// 取反定向结果
Map<String, Map<String, BitSet>> disInx = bitSetIndex.getDescIndex().get("-");
// 与正定向的差异 1、不需要处理通投 2、维度之前是与的关系,满足其中一个反定向都不能再投
if (disInx != null) {
    BitSet dis = getNegativeResult(conditionMap, disInx);
    if (dis != null && dis.cardinality() > 0) {
        dis.flip(0, size);
        poi.and(dis);
    }
}
// 查找到的规则数量
int resultCount = poi.cardinality();
List<Object> resultCastIds = new ArrayList<Object>(resultCount);
int i = 0;
    }

总结

我们通过BitSet的or方法解决了实际生产中遇到不包含的问题,通过BitSet的flip方法,and方法解决了正反定向相交的问题~

新需求,在设置站长等级规则时,需要新增如下两个维度

一: 支付金额额度,维度值是一个范围,规定了最小支付金额和最大支付金额

二: 下单url维度,维度规定了url的前后缀

那如果一个订单来查询它的站长等级时,除了要满足前面所说的 推广内容 1 推广渠道 2 点击平台 3 下单平台 4 外,还需要满足支付金额的限定,以及下单url的限定。

思路

还以之前的例子为例

站长等级表user_level_rule(id, rule_name, start_date, end_date, union_ids, union_level, cookie_expire)

站长等级维度表 user_level_rule_dimension(id, rule_id, type, type_value)

其中维度表的 rule_id 就是 站长等级表的 id。

type枚举:推广内容 1 推广渠道 2 点击平台 3 下单平台 4

type_value枚举:推广内容(单品 1、店铺 2)、推广渠道(站长端 1)、点击平台(微信 1、QQ 2)、下单平台(极速版app 1)

按上述的例子,会生成如下记录:

user_level_rule (100, '定向指定中等级站长', 20221001, 20221201, '52267', 3, '');

user_level_rule_dimension(1, 100, 1, 1);

user_level_rule_dimension(2, 100, 1, 2);

user_level_rule_dimension(3, 100, 2, 1);

user_level_rule_dimension(4, 100, 3, 1);

user_level_rule_dimension(5, 100, 3, 2);

user_level_rule_dimension(6, 100, 4, 1);

新增限定范围的维度和限定前后缀的维度,我们很容易想到新增type枚举值,新增后如下

type枚举:推广内容 1 推广渠道 2 点击平台 3 下单平台 4 支付金额 5 下单url 6

那对应的范围值和前后缀该如何存储呢?

新增范围维度表:range_dimension(id, min, max)

新增前后缀维度表:url_dimension(id, prefix, suffix)

当在创建一个规则时,设置了支付金额和下单url维度,那我们将范围数据和前后缀数据写入到对应的range_dimension、url_dimension表中,在user_level_rule_dimension表中新增type为5和6的记录,typeValue分别为range_dimension、url_dimension表的id。

可以看到,上述两种情况,user_level_rule_dimension维度值存储是另外两张表的主键,实际对维度已有的索引构建及匹配逻辑是没有影响的,唯一需要额外关注的是:范围信息和前后缀信息该如何存储,以及如何找到对应的 id,下面我们逐个来说~

限定范围

我们将范围信息抽象为如下结构:

vbnet 复制代码
    public class Range implements Serializable {
private String id;
private Double min;
private Double max;
    }

在构建前面倒排索引的时候,同时也构建 Map<String, List> rangeIndex 这样的一个结构,维护到索引的一个字段上,Map的key为一个固定值 "range"。

在对请求信息构建conditionMap时,也将支付金额作为维度"range"写入到 conditionMap中

在匹配时,我们遍历 rangeIndex,判断请求conditionMap中是否包含"range"维度,若包含的话,判断conditionMap中的value值是否在对应索引的范围内,若在的话,我们查询得到的rangeId作为支付维度的值写入到conditionMap中~

下面来看看如何实现

构建请求conditionMap:

Map<String, List> conditionMap = new HashMap<String, List>(16);

generateCondition("range", reqVo.getPayPrice(), conditionMap);

typescript 复制代码
public static void generateCondition(String type, String value, Map<String, List<String>> conditionMap) {
List<String> valueList = conditionMap.get(type);
if (CollectionUtils.isEmpty(valueList)) {
    valueList = new ArrayList<String>();
}
valueList.add(value);
conditionMap.put(type, valueList);
}

查询rangeIndex,conditionMap 写入 "range"维度,值为找到的rangeId:

typescript 复制代码
 private void replaceRangeValue(Map<String, List<String>> conditionMap) {
  if (rangeIndex == null || rangeIndex.size() == 0) {
    return;
}
for (String rangeKey : rangeIndex.keySet()) {
    List<Range> rangeList = rangeIndex.get(rangeKey);
    List<String> conditionValueList = conditionMap.get(rangeKey);
    if (rangeList == null || conditionValueList == null) {
        continue;
    }
    Set<String> ids = new HashSet<String>();
    for (String value : conditionValueList) {
        try {
            double v = Double.parseDouble(value);
            for (Range range : rangeList) {
                boolean flag = (range.getMin() == null || v >= range.getMin())
                        && (range.getMax() == null || v <= range.getMax());
                if (flag) {
                    ids.add(range.getId());
                }
            }
        } catch (Exception e) {
            log.error("replaceRangeValue error, value:{}, error:{}",value, e.getMessage());
        }
    }
    conditionMap.put(rangeKey, new ArrayList<String>(ids));
}  
}

限定前后缀

限定前后缀,其实也和上面限定范围差异不大,我们前后缀信息抽象为如下结构:

typescript 复制代码
    public class UrlExpression implements Serializable {
private String id;
private String preFix;
private String subFix;
    }

思路和上面一样,UrlExpression的id作为user_level_rule_dimension下单url维度的值正常匹配,我们仅需要构建请求维度的url conditionMap以及单独构建 Map<String, List> expressionIndex 写入到我们的索引中。

构建请求conditionMap:

Map<String, List> conditionMap = new HashMap<String, List>(16);

generateCondition("domain", reqVo.getOrderUrl(), conditionMap);

typescript 复制代码
   public static void generateCondition(String type, String value, Map<String, List<String>> conditionMap) {   
List<String> valueList = conditionMap.get(type);
if (CollectionUtils.isEmpty(valueList)) {
    valueList = new ArrayList<String>();
}
valueList.add(value);
conditionMap.put(type, valueList);
    }

查询expressionIndex,conditionMap 写入 "domain"维度,值为找到的UrlExpressionId:

ini 复制代码
private void replaceExpValue(Map<String, List<String>> conditionMap) {
if (expressionIndex == null || expressionIndex.size() == 0) {
    return;
}
for (String directKey : expressionIndex.keySet()) {
    List<UrlExpression> directList = expressionIndex.get(directKey);
    List<String> conditionValueList = conditionMap.get(directKey);
    if (directList==null || conditionValueList == null) {
        continue;
    }
    Set<String> ids = new HashSet<String>();
    for (String value : conditionValueList) {
        if(StringUtils.isBlank(value)){
            continue;
        }
        String otherValue="";
        if(value.startsWith("http")){
            otherValue=value.replace("http:","https:");
        }
        if(value.startsWith("https")){
            otherValue=value.replace("https:","http:");
        }
        value=(value.contains("?"))?value.substring(0, value.indexOf("?")):value;
        try {
            for (UrlExpression exp : directList) {
                if ( value.startsWith(exp.getPreFix()) || (value+"/").startsWith(exp.getPreFix()) || otherValue.startsWith(exp.getPreFix()) || (otherValue+"/").startsWith(exp.getPreFix()) ) {
                    ids.add(exp.getId());
                }
            }
        } catch (Exception e) {
            log.error("replaceExpValue error, value:{}, error:{}",value, e.getMessage());
        }
    }
    //ids:UrlExpression的id
    conditionMap.put(directKey, new ArrayList<>(ids));
}
    }

至此,倒排索引在规则匹配上的应用全部就结束了

相关推荐
weixin_438335408 分钟前
springboot 中添加TCP连接服务端
spring boot·后端·tcp/ip
程序猿毕设源码分享网24 分钟前
基于springboot校园招聘系统源码和论文
java·spring boot·后端
山山而川粤39 分钟前
美食推荐系统|Java|SSM|JSP|
java·开发语言·后端·学习·mysql
小小药42 分钟前
012-spring的注解开发
java·后端·spring
AI人H哥会Java2 小时前
【Spring】基于XML的Spring容器配置——Bean的作用域
java·开发语言·后端·spring·架构
vvw&2 小时前
如何在 Ubuntu 22.04 上安装 Elasticsearch
linux·运维·服务器·后端·ubuntu·elasticsearch·搜索引擎
uhakadotcom2 小时前
2025年java技术发展趋势展望
java·后端·架构
uhakadotcom2 小时前
2025年大语言模型RAG技术趋势展望
后端·算法·架构
xiaocaibao7772 小时前
Rust语言的数据库编程
开发语言·后端·golang
LeonNo113 小时前
golang,多个proxy拉包的处理逻辑
开发语言·后端·golang