当前位置:   article > 正文

Redis新功能 RedisSearch安装和使用 媲美ES的存在_redis search

redis search

RedisSearch 简介

内容翻译自Redis官网
RedisSearch是一个强大的文本搜索和二级索引引擎,建立在Redis之上作为Redis模块。Redis搜索用C语言编写,与其他开源搜索引擎相比速度极快。它实现了多种数据类型和命令,从根本上改变了您可以使用Redis做什么。Redis搜索支持搜索和过滤功能,例如地理空间查询、仅检索ID(而不是整个文档)和自定义文档评分。聚合可以在自定义管道中组合map、filter和duce/grou-by操作,这些操作在瞬间运行数百万个元素。

RedisSearch安装

第一步:直接放编译后的二进制文件

编译文件是个技术活,会折腾很长时间,有时间的小伙伴可以去尝试一下。
链接:https://pan.baidu.com/s/1stMiYBCMIMcOUd-Qjbpasw
提取码:uhwt

第二步:挂载二进制文件

把redisearch.so拷贝到redis安装目录下的src目录,然后编辑redis.conf
挂载文件
loadmodule /data/redis-6.0.10/src/redisearch.so

最后: 重启redis即可

第三 操作方法 不同客户端

jedis

@Autowired
    private UnifiedJedis jedis;

    private String prefix = "$.";

    @PostConstruct
    private void init(){
        createIndex("place-index","place:", new String[]{"provinceName","cityList[*].cityName","cityList[*].geoinfo","cityList[*].countyList[*].countyName"});

    }

    public boolean createIndex(String indexName, String key, String... fields){

        try {

            try{
                Map<String, Object> map = jedis.ftInfo(indexName);
                log.info("index configuration:{}",map);
                jedis.ftDropIndex(indexName);
            } catch (Exception e){
                log.error("the index does not exist", e);
            }

            Schema schema = new Schema();

            float weight = 1.0f;
            for(String field : fields) {
                String attribute;
                if (StringUtils.isNoneBlank(field)) {

                    if (field.indexOf(".") == -1) {
                        attribute = field;
                    } else {
                        String[] fieldSplit = field.split("\\.");
                        attribute = fieldSplit[fieldSplit.length - 1];
                    }

                    if (attribute.toLowerCase().startsWith("geo")) {
                        Schema.Field field1 = new Schema.Field(FieldName.of(prefix + field).as(attribute), Schema.FieldType.GEO);
                        //schema.addGeoField(prefix + field);
                        schema.addField(field1);
                        continue;
                    } else {

                        Schema.TextField textField = new Schema.TextField(FieldName.of(prefix + field).as(attribute), weight, false, false, false, null);
                        schema.addField(textField);
                        weight *= 3;
                        continue;
                    }
                }
            }


            IndexDefinition rule = new IndexDefinition(IndexDefinition.Type.JSON).setLanguage("chinese")
                    .setPrefixes(new String[]{key});


            jedis.ftCreate(indexName,
                    IndexOptions.defaultOptions().setDefinition(rule),
                    schema);
            return true;
        } catch (Exception e){
            log.error("create redis search index failed", e);
            return false;
        }


    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

jedisSearch

@Autowired
    private Client client;

    /**
     * 删除索引
     */
    public void dropIndex(){
        // 只会删除索引,不会删除索引相关的文档,true:表示如果索引不存在,直接返回false,不报错
        client.dropIndex(true);
    }

    /**
     * 删除文档数据
     */
    public void deleteDocuments(String ...docs){
        // 传入一组(将删除的文档ID)
        client.deleteDocuments(true,docs);
    }

    /**修改文档**/
    public void updateDocument(String docId, double score, Map map){
        client.updateDocument(docId,score,map);
    }

    /**添加数据8**/
    public boolean addHashData(){
        try{
            // FullText类型(全文查询)
            Schema.Field storeName = new Schema.Field("storeName", Schema.FieldType.FullText,false);
            Schema.Field storeIntroduce = new Schema.Field("storeIntroduce", Schema.FieldType.FullText,false);
            // tag类型
            Schema.Field tag = new Schema.Field("tag", Schema.FieldType.Tag,false);
            // geo类型,经纬度
            Schema.Field location = new Schema.Field("location", Schema.FieldType.Geo,false);
            // number类型(此类型可以范围查询),true:允许排序
            Schema.Field start = new Schema.Field("start", Schema.FieldType.Numeric,true);

            // 定义一个索引模式(相当于student的表结构)
            Schema schema = new Schema()
                    .addField(storeName)
                    .addField(storeIntroduce)
                    .addField(tag)
                    .addField(location)// 加上Geo类型后,无法查询出数据,暂时不知道什么原因
                    .addField(start);


            // 创建索引(如果存在相同的索引则会创建失败)
            client.createIndex(schema, Client.IndexOptions.Default());

            // 创建一些map数据,准备存入student索引
            Map<String, Object> fields1 = createDocument("粥铺", "我家小米粥好喝", "满减"
                    ,new GeoValue(106.555697,29.613248,5000, GeoValue.Unit.METERS),300);

            Map<String, Object> fields2 = createDocument("米铺", "我家米便宜好吃", "香米"
                    ,new GeoValue(106.555661,29.613695,100, GeoValue.Unit.METERS),100);

            Map<String, Object> fields3 = createDocument("米通信", "电子批发城", "电子"
                    ,new GeoValue(106.555922,29.613429,100, GeoValue.Unit.METERS),120);

            Map<String, Object> fields4 = createDocument("熊猫家居", "只为让你有更好的舒适生活", "家居"
                    ,new GeoValue(106.555922,29.613429,100, GeoValue.Unit.METERS),220);

            Map<String, Object> fields5 = createDocument("河马家居", "为你私人定制", "家居"
                    ,new GeoValue(106.555571,29.612973,100, GeoValue.Unit.METERS),60);


            // 创建选项,设置语言为中文,否则无法进行中文分词查询
            AddOptions options = new AddOptions();
            options.setLanguage("chinese");

        /*
            创建文档(相当于表的每一行数据)id:"doc1"   fields:fields1  score:1
                id:唯一值,否则创建失败,且必须为string;
                fields:需要存放的数据(默认为Map类型);
                score:分数(权重0~1)
         */
            Document doc1 = new Document("doc1", fields1,1);
            Document doc2 = new Document("doc2", fields2,0.7);
            Document doc3 = new Document("doc3", fields3,0.5);
            Document doc4 = new Document("doc4", fields4,0.3);
            Document doc5 = new Document("doc5", fields5,0.1);

            // 添加文档,将设置项加进去
            client.addDocument(doc1,options);
            client.addDocument(doc2,options);
            client.addDocument(doc3,options);
            client.addDocument(doc4,options);
            client.addDocument(doc5,options);
        }catch (Exception e){
            e.printStackTrace();
            log.info("文档添加失败!");
            return false;
        }
        log.info("文档添加成功!");
        return true;
    }

    /**根据关键字全文查询
     *  @method limit(0,3) 从位置0开始查询出3条
     *  @method setWithScores() 按分数大小查询(权重),从大到小排
     *  @method setLanguage("chinese") 默认不支持中文分词,所以要设置语言
     *  @method highlightFields("title","body") 设置哪些字段需要高亮显示
     */
    public SearchResult searchByKey(String queryString) {

        // 创建查询语句
        Query query = new Query(queryString)
                .limit(0,3)
                .setWithScores()
                .highlightFields("storeName","storeIntroduce")
                .setLanguage("chinese");

        // 提交查询
        SearchResult result = client.search(query);
        // 输出
        show(result);
        return result;
    }

    /**根据数值范围+key查询
     *
     * @method addFilter()
     *      添加过滤器
     * @method Query.NumericFilter(field,min,max)
     *      数值过滤器
     *      查询字段field范围(min,max)
     * **/
    public SearchResult searchByNumberRange(String key, String field, int min, int max){

        Query query = new Query(key)
                .addFilter(new Query.NumericFilter(field,min,max))
                .limit(0,3)
                .setLanguage("chinese");
        // 提交查询
        SearchResult result = client.search(query);
        // 输出
        show(result);
        return result;
    }

    /**位置范围+key查询
     * new Query.GeoFilter("location",lon,lat,radius,unit)
     *      经纬度过滤器
     * @param key 搜索关键词
     * @param lon 经度
     * @param lat 纬度
     * @param radius 半径
     * @param unit 度量单位
     * **/
    public SearchResult searchByGeoRange(String key, double lon, double lat, double radius, String unit){
        Query query = new Query(key)
                .addFilter(new Query.GeoFilter("location",lon,lat,radius,unit))
                .limit(0,3)
                .setLanguage("chinese");
        // 提交查询
        SearchResult result = client.search(query);

        show(result);
        return result;
    }

    /**
     * 聚合查询
     * @param query
     * @param start
     * @param state
     * @param avgprice
     * @param k
     * @return
     */
    public AggregationResult aggregate(String query, String start, String state, String avgprice, String k){
        AggregationBuilder builder =new AggregationBuilder(query)
                .apply("@".concat(start).concat("/1000"), k)
                .groupBy("@".concat(state), Reducers.avg("@".concat(k)).as(avgprice))
                .filter("@".concat(avgprice).concat(">=2"))
                .sortBy(Integer.MAX_VALUE, SortedField.asc("@".concat(state)));
        return client.aggregate(builder);
    }

    private static Map<String, Object> createDocument(String storeName, String storeIntroduce,
                                                      String tag, GeoValue location, Integer start){

        Map<String, Object> fields = new HashMap<>();

        fields.put("storeName", storeName);
        fields.put("storeIntroduce", storeIntroduce);
        fields.put("tag", tag);
        fields.put("location", location);
        fields.put("start", start);

        return fields;
    }

    public static void show(SearchResult searchResult){
        // 输出
        searchResult.docs.stream().forEach(System.out::println);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197

lettuceMod

 private final Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private RedisModulesClient client;

    /**
     * 创建索引 hash
     * @throws Exception
     */
    @PostMapping("/createIndex")
    public void createIndex() throws Exception {

        StatefulRedisModulesConnection<String, String> connection = client.connect();

        RedisModulesCommands<String, String> commands = connection.sync();
        String s = commands.ftCreate("beers", CreateOptions.<String, String>builder().on(CreateOptions.DataType.HASH).prefix("docs:").build(),
                Field.text("name").build());

        System.out.println(s);
    }

    /**
     * 删除索引
     */
    @PostMapping("/deleteIndex")
    public String deleteIndex(){
        StatefulRedisModulesConnection<String, String> connection = client.connect();

        RedisModulesCommands<String, String> commands = connection.sync();
        commands.ftDropindex("beers");
        return "索引删除成功";
    }

    /**
     * 新增数据
     */
    @PostMapping("/insertDataByHash")
    public String insertDataByHash(){

        StatefulRedisModulesConnection<String, String> connection = client.connect();

        RedisModulesCommands<String, String> commands = connection.sync();
        Map<String,String> map = new HashMap<>(2);
        map.put("name","dog");
        String hmset = commands.hmset("docs:2", map);
        return hmset;
    }

    /**
     * 搜索数据
     */
    @PostMapping("/searchData")
    public SearchResults searchData(){
        StatefulRedisModulesConnection<String, String> connection = client.connect();

        RedisModulesCommands<String, String> commands = connection.sync();

        SearchResults<String, String> documents = commands.ftSearch("beers", "*");

        return documents;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

redisson

@Autowired
    RedissonClient redisson;

    /**
     * 创建索引 hash类型
     */
    @PostMapping("/creatHashIndex")
    public String creatHashIndex(){


        RSearch s = redisson.getSearch();

        try {

            s.createIndex("idx", IndexOptions.defaults()
                            .on(IndexType.HASH)
                            .prefix(Arrays.asList("doc:")),
                    FieldIndex.text("v1"),
                    FieldIndex.text("v2"));

        }catch (Exception e){
            String message = e.getMessage();
            if(message.contains("Index already exists")){
                return "该索引已存在,请勿重复创建";
            }
            return "创建索引异常";

        }


        return "创建索引成功";
    }
    /**
     * 创建索引 json类型
     */
    @PostMapping("/creatJsonIndex")
    public String creatJsonIndex(){


        RSearch s = redisson.getSearch(StringCodec.INSTANCE);

        try {

            s.createIndex("idx", IndexOptions.defaults()
                            .on(IndexType.JSON)
                            .prefix(Arrays.asList("doc:")),
                    FieldIndex.numeric("$..arr").as("arr"),
                    FieldIndex.text("$..value").as("val"));

        }catch (Exception e){
            String message = e.getMessage();
            if(message.contains("Index already exists")){
                return "该索引已存在,请勿重复创建";
            }
            return "创建索引异常";

        }


        return "创建索引成功";
    }


    /**
     * 删除索引
     */
    @PostMapping("/dropIndex")
    public String dropIndex(){

        redisson.getSearch().dropIndex("idx");

        return "OK";
    }


    /**
     * 新增数据 hash类型
     * @return
     */
    @PostMapping("/insertDataByHash")
    public String insertDataByHash(){

        //rmap格式
        RMap<String, SimpleObject> m = redisson.getMap("doc:1", new CompositeCodec(StringCodec.INSTANCE, redisson.getConfig().getCodec()));
        m.put("v1", new SimpleObject("name1"));
        m.put("v2", new SimpleObject("name2"));
        RMap<String, SimpleObject> m2 = redisson.getMap("doc:2", new CompositeCodec(StringCodec.INSTANCE, redisson.getConfig().getCodec()));
        m2.put("v1", new SimpleObject("name3"));
        m2.put("v2", new SimpleObject("name4"));


        return "OK";
    }
    /**
     * 新增数据
     * @return
     */
    @PostMapping("/insertDataByJson")
    public String insertDataByJson(){

        RJsonBucket<TestClass> b = redisson.getJsonBucket("doc:1", new JacksonCodec<>(TestClass.class));
//        b.trySet(new TestClass(Arrays.asList(1, 2, 3), "hello"));
//        boolean hello = b.setIfAbsent(new TestClass(Arrays.asList(1, 2, 3), "hello"));
//        boolean hello = b.setIfExists(new TestClass(Arrays.asList(1, 2, 3), "hello"));
        b.set(new TestClass(Arrays.asList(1, 2, 3), "hello"));


        return "新增成功";
    }

    /**
     * 查询数据 hash
     * @return
     */
    @PostMapping("/searchDataByHash")
    public SearchResult searchDataByHash(){


        RSearch s = redisson.getSearch();

        SearchResult r = s.search("idx", "*", QueryOptions.defaults()
                .returnAttributes(new ReturnAttribute("v1"), new ReturnAttribute("v2")));

        return r;
    }
    /**
     * 查询数据 json
     * @return
     */
    @PostMapping("/searchDataByJson")
    public SearchResult searchDataByJson(){

        RSearch s = redisson.getSearch(StringCodec.INSTANCE);

        SearchResult r = s.search("idx", "*", QueryOptions.defaults()
                .returnAttributes(new ReturnAttribute("arr"), new ReturnAttribute("val")));

        return r;
    }

    /**
     * Aggregation聚合查询
     */
    @PostMapping("/searchAggregationDataByHash")
    public AggregationResult searchAggregationDataByHash(){


        RSearch s = redisson.getSearch();

        AggregationResult r = s.aggregate("idx", "*", AggregationOptions.defaults()
                .load("v1", "v2"));

        return r;
    }
    /**
     * 查询数据 json
     * @return
     */
    @PostMapping("/searchAggregationDataByJson")
    public AggregationResult searchAggregationDataByJson(){

        RSearch s = redisson.getSearch(StringCodec.INSTANCE);

        AggregationResult r = s.aggregate("idx", "*", AggregationOptions.defaults()
                .load("arr", "val"));

        return r;
    }

    /**
     * 拼写检查
     * FT. SPELLCHECK返回一个数组回复,其中每个元素代表查询中的一个拼写错误的术语。
     * 拼写错误的术语按其在查询中出现的顺序排序。
     * 每个拼写错误的术语依次是一个3元素数组,由常量字符串TERM、术语本身和拼写更正建议数组组成。
     * 拼写更正数组中的每个元素由建议的分数和建议本身组成。
     * 每个拼写错误的术语的建议数组按分数降序排列。
     * 分数的计算方法是将建议术语存在的文档数量除以索引中的文档总数。
     * 结果可以通过将分数除以最高分来归一化。
     */
    @PostMapping("/spellCheck")
    public Map<String, Map<String, Double>> spellCheck(){

        RSearch s = redisson.getSearch();


        long l = s.addDict("name", "hockey", "stik");

        Map<String, Map<String, Double>> res = s.spellcheck("idx", "hocke sti", SpellcheckOptions.defaults()
                .includedTerms("name").distance(2));

        return res;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192

redis-spring-search

 @Autowired
    StatefulRediSearchConnection statefulRediSearchConnection;


    /**
     * 创建索引
     */
    @PostMapping("/createIndex")
    public String createIndex(){

        RediSearchCommands sync = statefulRediSearchConnection.sync();
        sync.create("pools",
                CreateOptions.builder().on(CreateOptions.Structure.HASH).prefix("beer:").build(),
                new Field.Text("name"));
        return "索引创建成功";
    }

    /**
     * 删除索引
     */
    @PostMapping("/dropIndex")
    public String dropIndex(){
        RediSearchCommands sync = statefulRediSearchConnection.sync();
        String pools = sync.dropIndex("pools");
        return pools;
    }

    /**
     * 新增数据
     * @return
     */
    @PostMapping("/insertDataHash")
    public Long insertDataHash(){

        RediSearchCommands sync = statefulRediSearchConnection.sync();
        Map<String,String > map = new HashMap<>(2);
        map.put("name","xiaobai");
        Long hset = sync.hset("beer:11", map);
        return hset;
    }

    /**
     * 搜索数据
     */
    @PostMapping("/searchData")
    public SearchResults searchData(){

        RediSearchCommands sync = statefulRediSearchConnection.sync();
        SearchResults pools = sync.search("pools", "xiaobai Schifrin");

        return pools;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/379338
推荐阅读
相关标签
  

闽ICP备14008679号